代码世界的“武林秘籍”:设计原则
| 设计原则 | 解释 | React 示例 | 反例 (React) | 体现在哪些设计模式中 |
|---|---|---|---|---|
| 单一职责原则 (SRP) | 一个零件只负责一个功能:就像一个螺丝只负责固定,而不负责装饰。 | 需求: 用户信息显示组件,数据加载和 UI 展示分离。 示例: 一个组件只负责展示用户数据,另一个组件用 Hooks 获取并传递数据给展示组件。 | 反例: 一个组件既加载数据又展示用户详情和编辑按钮,代码臃肿。 | 策略模式 (不同的验证策略)、命令模式 (不同的操作命令),组合模式(UI 组件的组合) |
| 开闭原则 (OCP) | 在添加新功能时,尽量不要修改现有的代码,而是通过扩展来实现:就像给房子加个阳台,但不需要改动房子结构。 | 需求: 可配置样式的按钮组件。 示例: 按钮组件使用 styled-components 根据type属性渲染出不同的样式,不需要修改按钮组件内部代码。 | 反例: 新增按钮样式时,需要直接修改按钮组件的代码,添加 if else 判断。 | 抽象工厂模式 (组件工厂创建不同的组件),装饰器模式(HOC)。 |
| 里氏替换原则 (LSP) | 当你设计子类(儿子)去继承父类(爸爸)时,必须保证子类的行为方式和父类是兼容的。任何使用父类的地方,都应该可以直接用子类替换,而程序的逻辑不会出错。子类是对父类行为的一种扩展或细化,而不是扭曲或颠覆。 | 假设你有一个基础的 Button 组件,它接受 onClick prop和 children prop 。你创建了一个更具体的 PrimaryButton 组件。好的做法是,PrimaryButton 要么内部 使用<Button onClick={props.onClick}>{props.children}</Button>,要么它自己也接受 onClick 和 children,并且行为与基础 Button 对这两个 prop 的预期一致 | 反例: 用 PrimaryButton 替换 Button,需要添加额外的 props 才能正常工作。 | 策略模式(不同的功能策略),模板方法模式(父类定义流程,子类实现细节) |
| 接口隔离原则 (ISP) | 一个组件不应该被迫依赖它不需要的方法; 也就是说,我们不应该为了切菜而使用炒锅,也不应该为了炒菜而使用菜刀,而是应该根据不同的需求,使用对应的工具 | 需求: 假设我们需要创建一个可以展示各种通知的组件,这些通知可能是成功消息、错误消息、警告消息等。每个通知都有一些通用的操作,比如显示和关闭,但不同类型的通知还会有一些独特的操作。 示例: 我们不直接定义一个包含所有操作的庞大接口,而是针对不同类型的通知,定义不同的接口,然后让组件根据自身需要,选择实现那些对应的接口。 | 反例: 我们定义一个庞大的 INotification 接口,包含所有可能的通知操作,然后让所有通知组件都实现这个接口,即使它们并不需要所有的方法。 | 接口隔离原则本身就是针对接口做的,所以这里没有特别符合的设计模式。 |
| 依赖倒置原则 (DIP) | 高层模块不直接依赖底层模块:电器只关心插座能提供电力,不关心电力来自发电厂还是太阳能板。 | 需求: 组件需要使用不同的数据源,例如 API 或者 LocalStorage。示例: 组件只依赖 DataProvider 接口,通过参数注入不同的Provider,切换数据源不需要修改组件代码。 | 反例: 组件内部直接使用具体的 fetch('/api/data') 或者 localStorage, 切换数据源需要更改代码。 | 工厂模式 (选择不同的数据源)、策略模式 (不同的数据获取策略) |
React 组件中的设计模式体现
| 设计模式 | React 示例 | 解释 | 优点 |
|---|---|---|---|
| 观察者模式 | props 更新、useEffect 监听,第三方状态库(Redux、Zustand)。 | 当一个对象(被观察者)发生变化时,会自动通知所有依赖它的对象(观察者)。在 React 中,props 的更新会触发组件重新渲染,useEffect 可以监听状态变化并触发副作用,Redux 和 Zustand 等状态管理库也使用了观察者模式,当状态发生变化时更新组件。 | 解耦性强:被观察者和观察者之间不需要相互了解,使代码更加模块化;实时响应:当被观察者状态改变时,观察者会立即被通知并执行相应操作,实现高效的事件处理。 |
| 发布/订阅模式 | 假设我们正在开发一个在线文本编辑器: 文本编辑器组件 (TextEditor): 负责处理文本输入和编辑; 字数统计组件 (WordCounter): 负责实时显示字数; 拼写检查组件 (SpellChecker): 负责文本变更检查;历史记录组件 (HistoryTracker): 记录编辑历史。 这些组件需要响应 TextEditor 的文本变更事件,但不应有直接依赖关系。 | 发布/订阅模式是一种消息传递模式,发送者(发布者)不知道接收者(订阅者),有一个 消息中介 负责接收发布者的消息,并将消息传递给感兴趣的订阅者。 | 解耦性更强:发布者和订阅者完全解耦,无需知道对方的存在;灵活性高:可以动态增加、删除订阅者,无需修改发布者代码;可扩展性强:系统更容易添加新的功能模块。 |
| 单例模式 | 在 React 中,管理全局配置 (API 地址、主题、语言)时,通常需要确保只有一个配置实例。 | 单例模式是一种创建型模式,保证一个类在应用中只有一个实例,并提供全局访问点。 常用于管理全局配置、数据库连接池等需要全局唯一实例的场景。 | 节省资源:避免频繁创建和销毁实例;全局访问:提供统一访问点,方便获取唯一实例;数据共享:所有模块共享同一份实例数据,保持数据一致性。 |
| 工厂模式 | React.createElement 根据 type 参数创建 DOM 元素或组件实例。 | 工厂模式封装对象的创建逻辑,客户端代码无需关心对象创建细节,只需提供类型参数,工厂就创建对应的实例。React.createElement就是一个工厂函数, 它根据 type 创建各种 HTML 元素或者 React 组件 | 降低耦合:客户端代码与具体对象的创建逻辑解耦;提高代码复用性: 创建对象的逻辑集中管理,方便重用;易于扩展:可以方便地添加新的类型,而无需修改客户端代码。 |
| 装饰器模式 | 使用 withLogging 高阶组件为组件添加日志, withAuth 高阶组件用于权限验证。 | 装饰器模式在不修改原有对象结构的前提下,动态添加新的功能。在 React 中,高阶组件(HOC)接收一个组件作为参数,返回一个增强后的组件。 | 动态扩展:无需修改原有组件,而是通过装饰器动态添加新功能, 代码复用:可以把很多公用的能力放到一个装饰器中复用, 遵循开放-封闭原则: 对修改关闭,却可以对扩展开放,易于维护。 |
| 组合模式 | React 组件树结构,由多个组件嵌套组合而成。 | 组合模式将对象组合成树形结构来表示 “整体-部分” 的层级关系,使处理组合对象与处理单个对象一致。React 组件的嵌套是一种天然的体现。 | 清晰的层次结构:将复杂对象组织成树形结构,易于管理和维护;一致性: 可以像处理单个对象一样处理复杂的对象组合,简化代码;灵活性:方便添加或者删除组件,扩展性好。 |
| 迭代器模式 | React.Children.map 用于遍历 React 组件的子组件列表,并执行操作。 | 迭代器模式提供统一方式遍历集合元素,无需暴露集合内部结构。在 React 中 React.Children.map 隐藏了子组件的复杂结构,提供了统一的遍历方式,对每个子组件做操作。 | 统一遍历方式:为不同的集合结构提供统一的遍历接口; 隐藏内部实现:无需关心集合内部结构,只关注元素本身;分离职责:集合负责管理数据,迭代器负责遍历。 |
| 策略模式 | 根据不同的输入类型 (email、phone) 选择不同验证策略,根据角色选择不同按钮样式。 | 策略模式定义一组算法,每个算法封装在一个策略类中,客户端运行时选择合适策略。 在 React 中,组件可以根据场景选择不同的渲染和样式策略。 | 算法切换灵活:可以动态切换算法或策略,方便调整逻辑; 减少 if-else:避免大量 if-else 代码; 易于扩展:方便添加新的策略类,遵循 “开闭原则”。 |
| 命令模式 | Redux 的 action 对象只描述操作,不关心如何执行。reducer 负责执行操作。 | 命令模式包装请求为一个对象 (命令),可以在请求队列、日志请求中使用请求参数。Redux 的 action 描述了要做什么, 而 reducer 决定如何执行。 | 解耦请求和执行:请求者不关心执行者如何执行,降低模块耦合;支持撤销/重做:可以方便记录执行历史,实现撤销/重做功能; 易于扩展:可以方便添加新的命令,遵循开闭原则。 |
观察者模式和发布/订阅模式对比:
观察者模式:
- 直接通信: 观察者模式中,观察者直接订阅被观察者,当被观察者发生变化时,它直接通知所有已订阅的观察者。
- 紧耦合: 被观察者必须知道有哪些观察者在订阅自己,观察者也必须知道自己订阅了哪个被观察者。它们之间存在直接的依赖关系,耦合性较高。
- 同步通知: 一般情况下,被观察者会同步地通知所有观察者,也就是说通知过程会阻塞被观察者的执行流程。
发布/订阅模式 (Pub/Sub):
- 间接通信: 发布者(publisher)将消息发布到中介(message broker),而不是直接发送给订阅者。订阅者(subscriber)从这个中介订阅自己感兴趣的消息。
- 松耦合: 发布者不知道有哪些订阅者在订阅,订阅者也不知道有哪些发布者在发布。它们之间是通过中介进行通信的,彼此之间是解耦的。
- 异步通知: 发布者发布消息后,不需要等待订阅者处理,消息会被存储在中介中,订阅者可以在稍后时间异步地处理这些消息,从而实现更高的响应速度。
简单类比:
- 观察者模式: 你直接订阅报社的报纸,报社会直接把报纸送到你家。这是直接通信。
- 发布/订阅模式: 你订阅一个邮局的杂志订阅服务,你可以在邮局订阅多种报纸杂志。杂志社将杂志发送到邮局,然后邮局根据你的订阅列表将杂志发送到你家。这是通过邮局这个中介的间接通信。
主要区别总结:
特性 观察者模式 发布/订阅模式 通信方式 直接通信 间接通信(通过中介) 耦合性 紧耦合 松耦合 通知方式 通常同步 通常异步 参与者 被观察者、观察者 发布者、订阅者、中介 适用场景 对象的内部状态变化需要同步通知时 需要高度解耦和异步通信的场景 在实际应用中:
- 观察者模式: 适用于对象内部状态的同步通知,比如UI组件状态变化通知,事件监听等
- 发布/订阅模式: 适用于大型分布式系统,微服务架构中的消息传递,比如消息队列,事件总线等