Skip to content

设计原则—— SOLID 原则

约 1821 字大约 6 分钟

2024-11-28

在快速迭代的前端开发中,如何设计出既灵活又稳定的代码结构?SOLID 原则为我们提供了答案。

什么是 SOLID 原则?

SOLID 原则是面向对象编程和设计的五个基本原则,由 Robert C. Martin 提出。虽然起源于后端开发,但这些原则在前端架构设计中同样具有重要价值,特别是在现代前端框架如 React、Vue、Angular 的组件化开发中。

原则概览

SOLID 是五个设计原则首字母的缩写:

  • S - 单一职责原则 (Single Responsibility Principle)
  • O - 开闭原则 (Open/Closed Principle)
  • L - 里氏替换原则 (Liskov Substitution Principle)
  • I - 接口隔离原则 (Interface Segregation Principle)
  • D - 依赖倒置原则 (Dependency Inversion Principle)

单一职责原则 (SRP)

核心思想

一个类或模块应该只有一个引起变化的原因。

在前端中的应用

反例:承担过多职责的组件

违反 SRP 的组件
// 这个组件同时处理数据获取、用户交互和样式渲染
class UserProfile extends React.Component {
  state = { user: null, loading: true }

  async componentDidMount() {
    const response = await fetch('/api/user')
    const user = await response.json()
    this.setState({ user, loading: false })
  }

  handleEdit = () => {
    // 编辑逻辑
  }

  handleDelete = () => {
    // 删除逻辑
  }

  render() {
    if (this.state.loading)
      return <div>Loading...</div>

    return (
      <div className="profile-card">
        <img src={this.state.user.avatar} alt="avatar" />
        <h2>{this.state.user.name}</h2>
        <p>{this.state.user.email}</p>
        <button onClick={this.handleEdit}>Edit</button>
        <button onClick={this.handleDelete}>Delete</button>
      </div>
    )
  }
}

改进后的设计

遵循 SRP 的组件拆分
// 数据获取职责
function withUserData(Component) {
  return class extends React.Component {
    state = { user: null, loading: true }

    async componentDidMount() {
      const user = await userService.getUser(this.props.userId)
      this.setState({ user, loading: false })
    }

    render() {
      return <Component {...this.props} {...this.state} />
    }
  }
}

// 展示职责
function UserProfileView({ user, onEdit, onDelete }) {
  return (
    <div className="profile-card">
      <img src={user.avatar} alt="avatar" />
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button onClick={onEdit}>Edit</button>
      <button onClick={onDelete}>Delete</button>
    </div>
  )
}

// 业务逻辑职责
const UserProfileContainer = withUserData(({ user, loading }) => {
  const handleEdit = () => { /* 编辑逻辑 */ }
  const handleDelete = () => { /* 删除逻辑 */ }

  if (loading)
    return <div>Loading...</div>

  return (
    <UserProfileView
      user={user}
      onEdit={handleEdit}
      onDelete={handleDelete}
    />
  )
})

开闭原则 (OCP)

核心思想

软件实体应该对扩展开放,对修改关闭。

实际应用

反例:需要频繁修改的组件

违反 OCP 的组件
function Notification({ type, message }) {
  if (type === 'success') {
    return <div className="success">{message}</div>
  }
  else if (type === 'error') {
    return <div className="error">{message}</div>
  }
  else if (type === 'warning') {
    return <div className="warning">{message}</div>
  }
  // 每次新增类型都需要修改这个组件
}

改进:可扩展的设计

遵循 OCP 的组件
// 定义通知类型映射
const notificationTypes = {
  success: ({ message }) => <div className="success">{message}</div>,
  error: ({ message }) => <div className="error">{message}</div>,
  warning: ({ message }) => <div className="warning">{message}</div>,
}

// 可扩展的通知组件
function Notification({ type, message }) {
  const NotificationComponent = notificationTypes[type]
  return NotificationComponent ? <NotificationComponent message={message} /> : null
}

// 扩展新的通知类型时无需修改原有组件
notificationTypes.info = ({ message }) => <div className="info">{message}</div>

里氏替换原则 (LSP)

核心思想

子类应该能够替换其父类,并且不会影响程序的正确性。

在前端组件中的应用

反例:违反替换原则的组件继承

违反 LSP 的组件设计
class BaseButton extends React.Component {
  render() {
    return <button onClick={this.handleClick}>{this.props.children}</button>
  }

  handleClick = () => {
    console.log('Button clicked')
  }
}

class SubmitButton extends BaseButton {
  handleClick = () => {
    if (!this.props.formValid) {
      throw new Error('Form is not valid') // 改变了父类的行为约定
    }
    super.handleClick()
  }
}

改进:使用组合而非继承

遵循 LSP 的组件设计
function BaseButton({ onClick, children, ...props }) {
  return <button onClick={onClick} {...props}>{children}</button>
}

function SubmitButton({ formValid, onSubmit, ...props }) {
  const handleClick = () => {
    if (formValid) {
      onSubmit()
    }
    else {
      console.warn('Form is not valid')
    }
  }

  return <BaseButton onClick={handleClick} {...props} />
}

接口隔离原则 (ISP)

核心思想

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

在 TypeScript 中的应用

反例:臃肿的接口

违反 ISP 的接口设计
interface UserAPI {
  getUser: (id: string) => Promise<User>
  createUser: (user: User) => Promise<void>
  updateUser: (user: User) => Promise<void>
  deleteUser: (id: string) => Promise<void>
  sendEmail: (user: User, message: string) => Promise<void>
  // 很多方法...
}

// 显示用户信息的组件被迫依赖整个接口
class UserProfile extends React.Component<{ userAPI: UserAPI }> {
  // 只使用了 getUser 方法,但被迫依赖其他不相关的方法
}

改进:细粒度的接口

遵循 ISP 的接口设计
interface UserReader {
  getUser: (id: string) => Promise<User>
}

interface UserWriter {
  createUser: (user: User) => Promise<void>
  updateUser: (user: User) => Promise<void>
  deleteUser: (id: string) => Promise<void>
}

interface EmailService {
  sendEmail: (user: User, message: string) => Promise<void>
}

// 组件只依赖需要的接口
class UserProfile extends React.Component<{ userReader: UserReader }> {
  // 现在只依赖真正需要的方法
}

依赖倒置原则 (DIP)

核心思想

高层模块不应该依赖于低层模块,二者都应该依赖于抽象。

实际应用

反例:直接依赖具体实现

违反 DIP 的组件
class UserService {
  async getUser(id) {
    const response = await fetch(`/api/users/${id}`)
    return response.json()
  }
}

class UserProfile extends React.Component {
  userService = new UserService() // 直接依赖具体实现

  async componentDidMount() {
    const user = await this.userService.getUser(this.props.userId)
    this.setState({ user })
  }
}

改进:依赖抽象

遵循 DIP 的组件
// 定义抽象
interface IUserService {
  getUser: (id: string) => Promise<User>
}

// 具体实现
class UserService implements IUserService {
  async getUser(id) {
    const response = await fetch(`/api/users/${id}`)
    return response.json()
  }
}

// 高层组件依赖抽象
class UserProfile extends React.Component<{ userService: IUserService }> {
  async componentDidMount() {
    const user = await this.props.userService.getUser(this.props.userId)
    this.setState({ user })
  }
}

// 依赖注入
const userService = new UserService()
ReactDOM.render(
  <UserProfile userService={userService} userId="123" />,
  document.getElementById('root')
)

在前端框架中的综合应用

React Hooks + SOLID

现代 React 中的 SOLID 实践
// 自定义 Hook - 单一职责
function useUser(userId) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    userService.getUser(userId).then(setUser).finally(() => setLoading(false))
  }, [userId])

  return { user, loading }
}

// 展示组件 - 开闭原则
function UserCard({ user, onAction, actionType = 'default' }) {
  const ActionComponent = actionComponents[actionType]

  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      {ActionComponent && <ActionComponent user={user} onAction={onAction} />}
    </div>
  )
}

// 业务组件 - 依赖倒置
function UserProfile({ userId, userService = defaultUserService }) {
  const { user, loading } = useUser(userId, userService)

  if (loading)
    return <div>Loading...</div>

  return <UserCard user={user} onAction={handleUserAction} />
}

Vue 3 Composition API

Vue 3 中的 SOLID 实践
<script setup>
// 单一职责 - 数据逻辑
const { user, loading } = useUser(props.userId)

// 单一职责 - 业务逻辑
const { handleEdit, handleDelete } = useUserActions(user)

// 依赖注入
provide('userService', userService)
</script>

<template>
  <UserCard
    :user="user"
    :loading="loading"
    @edit="handleEdit"
    @delete="handleDelete"
  />
</template>

实践建议与最佳实践

  • 从小处着手不要试图一次性应用所有原则,从最影响代码质量的痛点开始
  • 代码审查:在团队代码审查中加入 SOLID 原则的检查项
  • 渐进式重构:在维护现有项目时,逐步应用这些原则改进代码结构
  • 工具辅助:使用 ESLint、TypeScript 等工具帮助识别违反原则的代码模式
  • 平衡过度设计:避免为了原则而原则,保持代码的实用性和可读性

总结

SOLID 原则为前端开发提供了强大的设计指导:

  • 单一职责原则 帮助我们创建专注且可测试的组件
  • 开闭原则 使我们的代码易于扩展而无需修改现有实现
  • 里氏替换原则 确保组件之间的一致性和可替换性
  • 接口隔离原则 避免不必要的依赖,提高代码的模块化
  • 依赖倒置原则 促进松耦合和更好的可测试性

关键收获

SOLID 原则不是僵化的规则,而是帮助我们思考代码设计的工具。在前端开发中,这些原则与组件化、函数式编程等现代范式完美结合,能够显著提升代码的可维护性、可测试性和可扩展性。

通过合理应用 SOLID 原则,我们可以构建出更加健壮、灵活的前端架构,从容应对需求变化和技术演进。