开闭原则

开闭原则指的就是对扩展开放、对修改关闭。编写代码的时候不可避免地会碰到修改的情况,而遵循开闭原则就意味着当代码需要修改时,可以通过编写新的代码来扩展已有的代码,而不是直接修改已有代码本身对扩展开放,对修改封闭

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,因为:

  1. 添加新变体需要修改组件
  2. 组件需要了解所有可能的变体
  3. 每次添加测试都会变得更加复杂

构建开放式组件

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);