Skip to content

SOLID 原则在前端的应用

November 15, 2024 by ccforeverd

简介 SOLID 原则在前端的应用

SOLID 用于面向对象编程 (OOP), 旨在解决软件开发中的复杂性和维护问题

  1. 单一职责 Single Responsibility Principle - SRP
  2. 开闭原则 Open/Closed Principle - OCP
  3. 里氏替换 Liskov Substitution Principle - LSP
  4. 接口隔离 Interface Segregation Principle - ISP
  5. 依赖反转 Dependency Inversion Principle - DIP

单一职责原则

一个类或模块应只有一个发生变化的原因, 仅负责一项特定功能

错误示例: (可增加)

  • 一个组件承担了太多责任, 既负责 UI 渲染, 又负责业务逻辑和数据请求

修改方式: (可增加)

  • 复杂 UI 组件拆分成多个小组件
  • 使用自定义 hook 拆分业务逻辑

优点: (可增加)

  • 职责明确
  • 提高复用性
  • 测试更加方便
  • 代码扩展更加灵活

开闭原则

软件实体应能在不修改原有代码的情况下扩展其行为, 即 对扩展开放, 对修改封闭

举例: (可增加)

  • 表单验证
    • 错误 (验证逻辑集中在函数内, 不可扩展, 如需修改必须改动 validateForm 函数)

      function validateForm<T>(values: T) {
        const errors: Partial<Record<keyof T, string>> = {}
        if (!values.name) {
          errors.name = 'Name is required'
        }
        if (!values.email) {
          errors.email = 'Email is required'
        } else if (!isEmail(values.email)) {
          errors.email = 'Email is invalid'
        }
        return errors
      }
    • 正确 (将验证逻辑抽象为验证器, 同时传入数据和验证器进行表单校验)

      abstract class Validator {
        abstract validate(value: unknown): string | null
      }
      class NameValidator extends Validator {
        validate(value: string | undefined) {
          return value ? null : 'Name is required'
        }
      }
      class EmailValidator extends Validator {
        validate(value: string | undefined) {
          if (!value) {
            return 'Email is required'
          }
          return isEmail(value) ? null : 'Email is invalid'
        }
      }
      function validateForm<T>(values: T, validators: Record<keyof T, Validator>) {
        const errors: Partial<Record<keyof T, string>> = {}
        for (const key in validators) {
          const error = validators[key].validate(values[key])
          if (error) {
            errors[key] = error
          }
        }
        return errors
      }
      validateForm({ name: '', email: 'test' }, {
        name: new NameValidator(),
        email: new EmailValidator()
      })

里氏替换原则

子类必须能够替换基类, 派生类或组件应该能够替换基类, 而不会影响程序的正确性

举例: (可增加)

  • Button 行为不一致
    • 错误 (LinkButton 无法替换 Button, 因为其没有 onClick 属性)

      function Button({ onClick }) {
        return <button onClick={onClick}>Click me</button>
      }
      function LinkButton({ href }) {
        return <a href={href}>Click me</a>
      }
       
      <Button onClick={() => {}} />
      <LinkButton href="#" />
    • 正确 (使用 Clickable 组件对两者进行封装)

      function Clickable({ children, onClick }) {
        return <div onClick={onClick}>{children}</div>
      }
      function Button({ onClick }) {
        return (
          <Clickable onClick={onClick}>
            <button>Click me</button>
          </Clickable>
        )
      }
      function LinkButton({ href }) {
        return (
          <Clickable onClick={() => (window.location.href = href)}>
            <a href={href}>{children}</a>
          </Clickable>
        )
      }
       
      <Clickable onClick={() => {}}>Click me</Clickable>
      <Link href="#">Click me</Link>

接口隔离原则

客户端不应该被迫依赖他们不使用的接口

这个应该是我用的最多的, 因为不想写没用的代码, 也不想引入没用的代码

(原文例子感觉一般, 可增加)

依赖倒置原则

高级模块不应该依赖于低级模块, 两者都应该依赖于抽象 (例如接口)

举例: (可增加)

  • fetchData
    • 错误 (直接在组件内部调用 fetchData, 无法复用)

      function fetchData() {
        return fetch('https://api.example.com/data')
      }
      function App() {
        const [data, setData] = useState()
        useEffect(() => {
          fetchData().then((res) => setData(res))
        }, [])
        return <div>{data}</div>
      }
    • 正确 (将 fetchData 抽象为 fetcher, 传入 fetcher 进行数据请求)

      interface Fetcher {
        fetch(url: string): Promise<any>
      }
      class FetcherImpl implements Fetcher {
        fetch(url: string) {
          return fetch(url)
        }
      }
      function App({ fetcher }: { fetcher: Fetcher }) {
        const [data, setData] = useState()
        useEffect(() => {
          fetcher.fetch('https://api.example.com/data').then((res) => setData(res))
        }, [])
        return <div>{data}</div>
      }
      <App fetcher={new FetcherImpl()} />