开闭原则
开闭原则指的就是对扩展开放、对修改关闭。编写代码的时候不可避免地会碰到修改的情况,而遵循开闭原则就意味着当代码需要修改时,可以通过编写新的代码来扩展已有的代码,而不是直接修改已有代码本身。对扩展开放,对修改封闭
function validate() {
// 校验用户名
if (!username) ... else ...
// 校验密码
if (!pswd) ... else ...
}如果此时增加一个校验条件,就要修改 validate() 函数内容。
class Validation {
private validateHandlers: ValidateHandler[] = [];
public addValidateHandler(handler: IValidateHandler) {
this.validateHandlers.push(handler)
}
public validate() {
for (let i = 0; i < this.validateHandlers.length; i++) {
this.validateHandlers[i].validate();
}
}
}
interface IValidateHandler {
validate(): boolean;
}
class UsernameValidateHandler implements IValidateHandler {
public validate() {
...
}
}React 中的开闭原则
用 React 的术语来说:组件应该易于扩展,而无需更改其现有代码。让我们看看这在实践中是如何发挥作用的。
常见的错误方式
// DON'T DO THIS
const Button = ({ label, onClick, variant }: ButtonProps) => {
let className = "button";
// Direct modification for each variant
if (variant === "primary") {
className += " button-primary";
} else if (variant === "secondary") {
className += " button-secondary";
} else if (variant === "danger") {
className += " button-danger";
}
return (
<button className={className} onClick={onClick}>
{label}
</button>
);
};这违反了 OCP,因为:
- 添加新变体需要修改组件
- 组件需要了解所有可能的变体
- 每次添加测试都会变得更加复杂
构建开放式组件
type ButtonBaseProps = {
label: string;
onClick: () => void;
className?: string;
children?: React.ReactNode;
};
const ButtonBase = ({
label,
onClick,
className = "",
children,
}: ButtonBaseProps) => (
<button className={`button ${className}`.trim()} onClick={onClick}>
{children || label}
</button>
);
// Variant components extend the base
const PrimaryButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-primary" />
);
const SecondaryButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-secondary" />
);
const DangerButton = (props: ButtonBaseProps) => (
<ButtonBase {...props} className="button-danger" />
);组件合成模式
更复杂的示例:
type CardProps = {
title: string;
children: React.ReactNode;
renderHeader?: (title: string) => React.ReactNode;
renderFooter?: () => React.ReactNode;
className?: string;
};
const Card = ({
title,
children,
renderHeader,
renderFooter,
className = "",
}: CardProps) => (
<div className={`card ${className}`.trim()}>
{renderHeader ? (
renderHeader(title)
) : (
<div className="card-header">{title}</div>
)}
<div className="card-content">{children}</div>
{renderFooter && renderFooter()}
</div>
);
// Extended without modification
const ProductCard = ({ product, onAddToCart, …props }: ProductCardProps) => (
<Card
{…props}
renderFooter={() => (
<button onClick={onAddToCart}>Add to Cart - ${product.price}</button>
)}
/>
);用于扩展的高阶组件 -HOC
type WithLoadingProps = {
isLoading?: boolean;
};
const withLoading = <P extends object>(
WrappedComponent: React.ComponentType<P>
) => {
return ({ isLoading, …props }: P & WithLoadingProps) => {
if (isLoading) {
return <div className="loader">Loading…</div>;
}
return <WrappedComponent {…(props as P)} />;
};
};
// Usage
const UserProfileWithLoading = withLoading(UserProfile);