当面试官问你什么是设计原则时
在学习设计模式时,一定会学到 SOLID 设计原则,分别是:单一职责原则、开放封闭原则、里氏替换原则、接口隔离原则、依赖反转原则。
在许多学习资料中都是以"类"的设计来解释这些原则,那么作为前端开发该如何理解和实践者几种原则呢?
前端多以页面开发为主,页面由 N 个组件搭建而成,可以把组件理解为"类"来学习和理解 SOLID 设计原则,下面会以 React 为示例,看看在日常开发中是如何应用设计原则的。
单一职责原则(Single Responsibility Principle)
定义:任何一个软件模块都应该只对某一类行为者负责
放在前端:每个模块只做特定的事情,例如 UI 组件只负责渲染、过滤逻辑只写在过滤模块里等。
下面以 React 示例代码讲解下:
import { useEffect, useState, useMemo } from "react";
import getFilmsApi from "../http/getFilms";
import { Card, Space, Rate } from "antd";
const BadSRP = () => {
const [films, setFilms] = useState([]);
const [filterRate, setFilterRate] = useState(1);
const fetchFilms = async () => {
const data = await getFilmsApi();
data && setFilms(data);
};
useEffect(() => {
fetchFilms();
}, []);
const onRating = (rate) => {
setFilterRate(rate);
};
const filterFilms = useMemo(() => {
return films.filter((film) => film.starCount >= filterRate);
}, [films, filterRate]);
return (
<>
<div style={{ marginBottom: "20px" }}>
<Rate value={filterRate} onChange={onRating} />
</div>
<Space wrap>
{filterFilms.map((film) => (
<Card
size="small"
hoverable
style={{ width: 120 }}
cover={<img alt="example" src={film.cover} />}
>
<Card.Meta
title={film.name}
description={
<Rate
style={{ fontSize: "12px" }}
disabled
defaultValue={film.starCount}
/>
}
/>
</Card>
))}
</Space>
</>
);
};
export default BadSRP;
上面展示了一段示例代码和对应的页面,其功能也很简单:页面初始化的时候拉取电影列表并展示,用户可以通过星标来筛选电影列表。
代码很简单也能正常运行,但是显然将页面所有逻辑放在一个组件里面是不符合单一职责的,我们需要将其进行拆分。
首先,我们将页面拆分成 FilterRate 和 FilmList 两个组件:
// FilterRate Component
import { Rate } from "antd";
export const filterFilms = (films, rate) => {
return films.filter((film) => film.starCount >= rate);
};
export const FilterRate = (props) => {
const { filterRate, onRating } = props;
return <Rate value={filterRate} onChange={onRating} />;
};
// FilmList Component
import { Space, Card, Rate } from "antd";
export default (props) => {
const { films } = props;
return (
<Space wrap>
{films.map((film) => (
<Card
size="small"
hoverable
style={{ width: 120 }}
cover={<img alt="example" src={film.cover} />}
>
<Card.Meta
title={film.name}
description={
<Rate
style={{ fontSize: "12px" }}
disabled
defaultValue={film.starCount}
/>
}
/>
</Card>
))}
</Space>
);
};
这里要注意两点:
- 我们没有将业务逻辑写在组件里面,因为业务逻辑是可以复用的,而 UI 组件只需要负责渲染即可
- 我们将 filterFilms 功能函数放在了 FilterRate 文件里面,是因为这个函数的作用是过滤电影的规则,按照只为某一类(过滤)负责的原则,将其放在这里
然后我们要将业务逻辑做一次抽离,将可复用逻辑写成 useFilter 和 useFilms 两个 hooks:
// useFilter:负责过滤业务逻辑
import { useState } from "react";
export default () => {
const [filterRate, setFilterRate] = useState(1);
const onRating = (rate) => {
setFilterRate(rate);
};
return {
filterRate,
onRating
};
};
// useFilms:负责电影数据拉取
import { useEffect, useState } from "react";
import getFilmsApi from "../../http/getFilms";
export default () => {
const [films, setFilms] = useState([]);
const fetchFilms = async () => {
const data = await getFilmsApi();
data && setFilms(data);
};
useEffect(() => {
fetchFilms();
}, []);
return {
films
};
};
抽离之后的代码将业务逻辑、UI 进行了隔离,并且每一块内容只做特定的事情:
// page.jsx
import { FilterRate } from "./FilterRate";
import FilmList from "./FilmList";
import useFilms from "./hooks/useFilms";
import useFilter from "./hooks/useFilter";
import { filterFilms } from "./FilterRate";
export default () => {
const { films } = useFilms();
const { filterRate, onRating } = useFilter();
return (
<>
<FilterRate filterRate={filterRate} onRating={onRating} />
<FilmList films={filterFilms(films, filterRate)} />
</>
);
};
开放封闭原则(Open-Closed Principle)
定义:设计良好的计算机软件应该易于拓展,同时抗拒修改
放在前端:组件要拓展的时候,尽可能地不要修改组件内部。
下面以 React 示例代码讲解下:
import { Button } from "antd";
import { RightOutlined, LeftOutlined } from "@ant-design/icons";
interface IProps {
text: string;
rule?: "back" | "forword";
}
export default (props: IProps) => {
const { text, rule } = props;
// 存放不同形态的 icon
const ruleIcon = {
back: <LeftOutlined />,
forword: <RightOutlined />
};
return <Button icon={ruleIcon[rule] ?? null}>{text}</Button>;
};
上面这段代码写的是一个 Button 组件,组件的接口对应了两种形态:back 和 forword,每个形态都对应不同的 icon。
如果此时要对组件进行拓展,新增一个 moveUp 形态,那么就需要更改到组件内的代码,新增 ruleIcon 的属性,显然这样是违背"开放封闭原则"的。
我们将这段代码优化下:
import { Button } from "antd";
import { ReactNode } from "react";
interface IProps {
text: string;
icon?: ReactNode;
}
export default (props: IProps) => {
const { text, icon } = props;
return <Button icon={icon ?? null}>{text}</Button>;
};
改动之后,Button 组件的 icon 由外部传入,此时再新增形态,就无需修改组件内部代码。
里氏替换原则(Liskov Substitution Principle)
定义:程序中的对象都应该能够被各自的子类实例替换,而不会影响到程序的行为
放在前端:衍生组件可以替换父组件,而不会影响程序运行。
下面以 React 示例代码讲解下:
// in Button.tsx
import { Button, ButtonProps } from "antd";
export interface IButtonProps extends ButtonProps {}
export default (props: IButtonProps) => {
const { ...restProps } = props;
return <Button {...restProps}></Button>;
};
// in RedButton.tsx
import Button, { IButtonProps } from "./Button";
interface IRedButtonProps extends IButtonProps {}
export default (props: IRedButtonProps) => {
return <Button {...props} style={{ backgroundColor: "red" }}></Button>;
};
这里有两个组件,Button 和 RedButton。其中 RedButton 组件封装了 Button 组件,因此将 Button 组件替换成 RedButton 也不会有问题。
接口隔离原则(Interface Segregation Principle)
定义:软件设计不要依赖它不需要的东西
放在前端:不要传不必要的 props 给组件。
以电影卡片组件为例,假设我们定义了电影的数据结构:
interface IFilm {
name: string;
cover: string;
starCount: number;
}
定义 Card 组件
import { Space } from "antd";
import Cover from "./Cover";
interface ICardProps {
film: IFilm;
}
export default (props: ICardProps) => {
const { film } = props;
return (
<Space direction="vertical">
<Cover film={film} />
<span>{name}</span>
</Space>
);
};
而 Card 组件内使用了 Cover 组件,用于展示电影封面,其对应的 props 接口为:
interface ICoverProps {
file: IFilm
}
这里 ICoverProps 接受整个 film 对象,但是 Cover 组件并不需要 film 的所有属性,它只需要一个封面链接,这里就违背了接口隔离原则。
我们需要将 Cover 组件接口改为只接受一个封面链接:
interface ICoverProps {
cover: string;
}
依赖反转原则(Dependency Inversion Principle)
定义:依赖抽象,而不是依赖具体的实现
放在前端:将业务逻辑控制进行抽离,UI 组件仅负责展示,并通过定义抽象接口来接收具体业务实现函数。
下面以 React 代码作为示例:
import { useState } from "react";
export default (props: IFormProps) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = () => {
// 发送请求.....
};
return (
<form action="" onSubmit={handleSubmit}>
<div>
<input
type="text"
value={username}
onChange={(e) => {
setUsername(e.target.value);
}}
placeholder="请输入用户名"
/>
</div>
<div>
<input
type="text"
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
placeholder="请输入密码"
/>
</div>
<div>
<button>提交</button>
</div>
</form>
);
};
上面这段代码是个简单的表单提交,输入用户名和密码然后提交表单。
如果此时有别的地方要复用这个表单,但是提交时需要做一些和原来不一样的处理,按照这种写法就需要给 props 新增标识,并且在 handleSubmit 中做判断。显然这是不合理的。
按照"依赖反转原则",表单组件只需要依赖抽象接口,这个接口接收 onSubmit 处理函数:
import { useState } from "react";
interface IFormProps {
onSubmit: (username: string, password: string) => void;
}
export default (props: IFormProps) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const { onSubmit } = props;
const handleSubmit = () => {
onSubmit(username, password);
};
return (
<form action="" onSubmit={handleSubmit2}>
<div>
<input
type="text"
value={username}
onChange={(e) => {
setUsername(e.target.value);
}}
placeholder="请输入用户名"
/>
</div>
<div>
<input
type="text"
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
placeholder="请输入密码"
/>
</div>
<div>
<button>提交</button>
</div>
</form>
);
};
这样的话我们就可以为 Form 表单封装多种不同 onSubmit 行为的表单了。
总结
前端开发中,都在时刻应用着设计原则:
- 单一职责:UI 和不同业务逻辑进行抽离
- 开放封闭:组件内部尽量保持稳定,易变动的内容由外部传入
- 里氏替换:衍生组件可以替换父组件(红色按钮可以替换普通按钮)
- 接口隔离:不要传无用的 props 给组件
- 依赖反转:组件的业务逻辑抽象为接口,由外部传入,从而可以封装成多种行为的组件
转载自:https://juejin.cn/post/7206208345606455357