为了提升代码的可读性,可维护,可测试。保证代码的整洁干净(clear code)是很重要的一件事
单一责任:每一个类(组件)只负责做一件事
SRP: every class should have only one responsibility
import { useEffect, useMemo, useState } from "react";
// 产品的类型,name,rate........
type ProductType = {
name: string;
rate: number;
[key: string]: any;
};
export default function Index() {
const [products, setProducts] = useState<ProductType[]>([]); // 产品的数据
const [filterRate, setFilterRate] = useState(1); // 过滤产品的🌟🌟等级数
const fetchProducts = async () => {
// 发一个请求,然后setProducts一下
const productsData = [
{
name: "产品1",
rate: 5,
},
{
name: "产品2",
rate: 4,
},
];
setProducts(productsData);
};
// 获取所有的产品
useEffect(() => {
fetchProducts();
}, []);
// 设置过滤的等级
const handleRating = (rate: number) => {
setFilterRate(rate);
};
// 过滤出来的产品
const filterProducts = useMemo(
() => products.filter((product) => product.rate >= filterRate),
[products, filterRate]
);
return (
<div>
{
<div>
这里是一个选择星星等级的dom,有个点击方法,调handleRating,选择过滤的等级
</div>
}
{filterProducts.map((product) => {
return <div>这里有三百行代码来渲染产品相关的内容</div>;
})}
</div>
);
}
这种页面很正常很普遍,我们一开始都是这么去写代码,请求数据,渲染内容,逻辑也不复杂。
但是从SOLID的角度他违反了单一职责的原则,我们来分析一下。⬇️
这个页面做了以下事情:
1.首先,把dom部分拆分成两个更小的组件
interface IFilterProps {
filterRate: number;
handleRating: (rate: number) => void;
}
// 这里是选择等级的组件,props接收两个参数,分别是要过滤的等级和选择等级的回调
function FilterRate(props: IFilterProps) {
return (
<div>
这里是一个选择星级的dom,有个点击方法,调handleRating,选择过滤的等级
</div>
);
}
export default FilterRate;
interface IProductProps {
product: {
rate: number;
name: string;
[key: string]: any;
};
}
function Product(props: IProductProps) {
return <div>这里有三百行代码来渲染产品相关的内容</div>;
}
export default Product;
return (
<div>
<FilterRate filterRate={filterRate} handleRating={handleRating} />
{filterProducts.map((product) => {
return <Product product={product} />;
})}
</div>
);
2.其次,数据的请求(一个useEffect,一个function,一个useState)需要一个自定义hook来处理
interface ProductType {
name: string;
rate: number;
[key: string]: any;
}
function useProducts() {
const [products, setProducts] = useState<ProductType[]>([]);
const fetchProducts = async () => {
// 发一个请求,然后setProducts一下
const productsData = [
{
name: "产品1",
rate: 5,
},
{
name: "产品2",
rate: 4,
},
];
setProducts(productsData);
};
// 获取所有的产品
useEffect(() => {
fetchProducts();
}, []);
return { products };
}
export default useProducts;
import { useMemo, useState } from "react";
import FilterRate from "./components/filterRate";
import Product from "./components/product";
import useProducts from "./hook/useProducts";
export default function Index() {
const { products } = useProducts(); // custom hook
const [filterRate, setFilterRate] = useState(1); // 过滤产品的🌟🌟级数
// 设置过滤的星级
const handleRating = (rate: number) => {
setFilterRate(rate);
};
// 过滤出来的产品
const filterProducts = useMemo(
() => products.filter((product) => product.rate >= filterRate),
[products, filterRate]
);
return (
<div>
<FilterRate filterRate={filterRate} handleRating={handleRating} />
{filterProducts.map((product) => {
return <Product product={product} />;
})}
</div>
);
}
3.数据的过滤也需要一个custom hook
function useRateFilter() {
const [filterRate, setFilterRate] = useState(1); // 过滤产品的🌟🌟级数
// 设置过滤的星级
const handleRating = (rate: number) => {
setFilterRate(rate);
};
return { filterRate, handleRating };
}
export default useRateFilter;
import { useMemo } from "react";
import FilterRate from "./components/filterRate";
import Product from "./components/product";
import useProducts from "./hook/useProducts";
import useRateFilter from "./hook/useRateFilter";
export default function Index() {
const { products } = useProducts(); // custom hook
const { filterRate, handleRating } = useRateFilter();
// 过滤出来的产品
const filterProducts = useMemo(
() => products.filter((product) => product.rate >= filterRate),
[products, filterRate]
);
return (
<div>
<FilterRate filterRate={filterRate} handleRating={handleRating} />
{filterProducts.map((product) => {
return <Product product={product} />;
})}
</div>
);
}
4.ok,还差一个useMemo过滤
其实就是一个过滤方法,可以把它写在FilterRate组件里,也可以写在Product组件里,也可以定义个utils文件夹,在里面写。这里选择在Product组件里写。
interface IProduct {
rate: number;
name: string;
[key: string]: any;
}
interface IProductProps {
product: IProduct;
}
export const filterProducts = (products: IProduct[], filterRate: number) => {
return products.filter((product) => product.rate >= filterRate);
};
export const Product = (props: IProductProps) => {
return <div>这里有三百行代码来渲染产品相关的内容</div>;
};
import FilterRate from "./components/filterRate";
import { Product, filterProducts } from "./components/product";
import useProducts from "./hook/useProducts";
import useRateFilter from "./hook/useRateFilter";
export default function Index() {
const { products } = useProducts();
const { filterRate, handleRating } = useRateFilter();
return (
<div>
<FilterRate filterRate={filterRate} handleRating={handleRating} />
{filterProducts(products, filterRate).map((product) => {
return <Product product={product} />;
})}
</div>
);
}
开闭原则:软件实体应该是可拓展的而不是可修改的
OCP: software entities should be open for extension but closed for modification"
type ButtonProps = {
role: "forward" | "back";
};
export default function Index(props: ButtonProps) {
const { role } = props;
return (
<div>
<button>这是一个按钮</button>
{role === "forward" && <div>button按钮后边跟一个前进的箭头</div>}
{role === "back" && <div>button按钮后边跟一个后退的箭头</div>}
</div>
);
}
我们开发一个Button组件,按钮后边需要跟一个图标,一开始计划是有前进图标或后退图标。
但是从SOLID的角度他违反了开闭原则⬇️
1.如果需要的不是前进后退的箭头,需要的是上箭头或者下箭头,那就要修改源代码。
type ButtonProps = {
// role: "forward" | "back";
icon: React.ReactNode;
};
export default function Index(props: ButtonProps) {
const { icon } = props;
return (
<div>
<button>这是一个按钮</button>
{/* {role === "forward" && button按钮后边跟一个前进的箭头}
{role === "back" && button按钮后边跟一个后退的箭头} */}
{icon}
</div>
);
}
直接传一个ReactNode,这样直接传一个icon即可。icon可以是任何图标,即满足了可拓展,又不需要修改源代码。
常见的如antd的Button组件的icon属性
里氏替换原则:子类型对象应该可以替代超类型对象
LSP: subtype objects should be substitutable for supertype objects
interface IInputProps {
placeholder?: string;
isBold?: boolean;
}
// 封装一个原生input组件,有个额外的参数isBold来控制字体是否加粗
export default function Index(props: IInputProps) {
const { isBold, placeholder = "这是封装的input组件" } = props;
return (
<div>
<input
type="text"
placeholder={placeholder}
style={{ fontWeight: isBold ? "bold" : "normal" }}
/>
</div>
);
}
没有满足里氏替换原则,子类对象(这个Index组件)不可以替代超类对象(原生input)。换句话说,封装的这个组件只替换了input的placeholder属性。再简单点说,封装的这个组件只能改个placeholder。
1.首先,要继承一下超类的属性
interface IInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
isBold?: boolean;
}
2.其次,解构剩余props,替换超类对象
interface IInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
isBold?: boolean;
}
export default function Index(props: IInputProps) {
const { isBold, placeholder = "这是封装的input组件", ...restProps } = props;
return (
<div>
<input
type="text"
placeholder={placeholder}
style={{ fontWeight: isBold ? "bold" : "normal" }}
{...restProps}
/>
</div>
);
}
接口分离原则:客户端不需要依赖于它不使用的接口(组件不应依赖于它用不到的props)
ISP: clients should not depend upon interfaces that they don’t use
让我们回到Single Responsibility中的产品组件⬇️
import Thumbnail from "./thumbnail";
interface IProduct {
rate: number;
name: string;
imageUrl: string;
[key: string]: any;
}
export interface IProductProps {
product: IProduct;
}
export const Product = (props: IProductProps) => {
const { product } = props;
return (
<div>
{/* 这里是个产品缩略图 */}
<Thumbnail product={product} />
这里有三百行代码来渲染产品相关的内容
</div>
);
};
新增的Thumbnail组件⬇️
import type { IProductProps } from "./product";
interface ThumbnailProps extends IProductProps {}
function Thumbnail(props: ThumbnailProps) {
return (
<div>
<img src={props.product.imageUrl} alt="" />
</div>
);
}
export default Thumbnail;
不符合接口分离原则
1.Thumbnail只需要产品里面的imageUrl属性,但他拿到了所有的product
<Thumbnail imageUrl={product.imageUrl} />
interface ThumbnailProps {
imageUrl: string;
}
function Thumbnail(props: ThumbnailProps) {
return (
<div>
<img src={props.imageUrl} alt="" />
</div>
);
}
export default Thumbnail;
依赖倒转原则:一个实体应该依赖于抽象而不是具体实现(组件应该更独立,更拓展,而不是为了具体的实现而重写)
DIP: one Entity should depend upon abstractions not concretions
import React, { useState } from "react";
function IForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// 发起登陆请求
await fetch("http://请求地址", {
body: JSON.stringify({
email,
password,
}),
});
};
return (
<div>
<form action="" onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</form>
</div>
);
}
export default IForm;
一个正常的登陆页面,但是如果要复用的话就会有问题,复用的登陆地址不是同一个。
因此他违背了依赖倒转原则:这个组件是为了具体实现而开发的。
具体实现的是———在这个请求地址登陆的。复用的时候需要另一个地址,难道在组件里写if-else吗,肯定不行的。那可以把地址传进来呀,但是不能保证在请求又有其它的逻辑。
import React, { useState } from "react";
function IForm(props: { onSubmit: (email: string, password: string) => void }) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { onSubmit } = props;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
onSubmit(email, password);
};
return (
<div>
<form action="" onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</form>
</div>
);
}
export default IForm;Ï
这样就可以在调用这个组件的时候通过onSubmit来进行不同的登陆接口的处理。
这样就可以说,这个组件的登陆实现依赖于onSubmit的回调(是抽象的),而不是具体在组件内部实现(具体的登陆接口写在组件内)。简单点说,就是不把登陆接口写死在组件内。
参考视频
感谢观看!!!