设计原则
介绍
设计原则是指导代码结构的思想,而设计模式是这些思想的具体实现方式。
SOLID 原则
- 单一职责原则(SRP)
- 开放封闭原则(OCP)
- 里氏替换原则(LSP)
- 接口隔离原则(ISP)
- 依赖倒置原则(DIP)
另外两个原则
- 迪米特法则(LoD)
- 合成复用原则(CRP)
1. 单一职责原则(SRP)⭐
单一职责原则(SRP:Single Responsibility Principle)
一个类(或模块/函数)只做一件事情。
设计模式
- 代理模式: 比如图片懒加载,原图片类只负责加载真实 DOM,代理类负责展示占位图和监听视口,各自职责单一
- 装饰器模式: 将核心逻辑和辅助逻辑(如日志记录、性能监控)剥离,通过装饰器组合
- 迭代器模式:把“数据存储”和“数据遍历”解耦,严格遵守单一职责原则
- 外观模式:虽然它“涉及”了很多事情,但它自己不负责实现任何具体的业务逻辑
前端应用
- React:组件拆分(Container / UI)
- Vue:composition API(逻辑拆分)
- hooks:一个 hook 一个功能
2. 开放封闭原则(OCP)⭐
开放封闭原则(OCP:Open Closed Principle)
可以扩展的,但不可以修改。
开闭原则的核心不是“不能改代码”,而是“让变化通过扩展来实现,而不是修改已有逻辑”。
设计模式
- 策略模式:新增策略,不修改原代码
- 观察者/发布订阅:新增订阅者,不修改原逻辑
- 装饰器模式:扩展功能,不修改原对象
- 代理模式:扩展功能,不修改原对象
- 模板方法模式:流程固定,步骤可扩展
- 职责链模式:新增处理逻辑,只需增加节点
前端应用
- 表单校验规则
- 支付方式
- UI 主题切换
- 插件系统
3. 里氏替换原则(LSP)
里氏替换原则(LSP:Liskov Substitution Principle)
“如果你声称自己继承了老爸,那你老爸能干的活,你必须都能干,而且不能把事情搞砸;你可以比老爸多干活,但绝不能少干活或乱干活。”
子类只能去“扩展”父类的能力,而绝对不能去“阉割”或“改变”父类已经承诺好的行为!
反面例子:企鹅算不算鸟?
在生物学上:企鹅是鸟。
但是在面向对象编程中,如果“鸟”定义了飞翔行为:企鹅绝不能继承鸟!
js
// 父类:鸟
class Bird {
fly() {
console.log("扑腾翅膀,飞向天空!");
}
}
// 子类:麻雀(完美继承老爸,遵守LSP)
class Sparrow extends Bird {
// 麻雀没重写飞,直接用老爸的飞
}
// 子类:企鹅(企鹅也是鸟,所以继承了鸟类)
class Penguin extends Bird {
// 但是企鹅不会飞!强行重写老爸的方法,抛出异常
fly() {
throw new Error("我是企鹅,我飞不起来啊!");
}
}
如何修改:
不能强行继承!应该提取更基础的父类。
- 基类:Bird(只包含吃饭、睡觉)
- 派生类1:FlyingBird 继承 Bird(增加 fly 方法)
- 派生类2:WalkingBird 继承 Bird(增加 walk 方法)
- 麻雀继承 FlyingBird,企鹅继承 WalkingBird。
设计模式
- 模板方法模式:子类扩展不破坏流程
- 策略模式:可替换算法
前端应用
- React 组件继承(不推荐乱继承),组件行为不能随意改变
- API 返回结构的一致性
- TS 类型设计
4. 接口隔离原则(ISP)
接口隔离原则(ISP:Interface Segregation Principle)
保持接口的单一独立,避免出现“胖接口”。
js
// ❌ 有些用户不需要上传
interface User {
login()
uploadVideo()
}
// ✅
interface Login {}
interface Upload {}
设计模式
- 适配器模式:专门用来“拆接口 / 转接口”,通过转换接口避免依赖不需要的字段
- 策略模式:不同用户用不同策略
前端应用
- TS 接口拆分
- API 类型定义
- hooks 设计
5. 依赖倒置原则(DIP)
依赖倒置原则(DIP:Dependence Inversion Principle)
面向接口编程,依赖于抽象而不依赖于具体,高层模块不依赖低层模块。
- 高层模块: 系统的核心业务逻辑(比如:订单结算、用户登录校验)。它决定了软件是干什么的,它是“老板”。
- 低层模块: 具体的基础工具/细节(比如:Axios 网络请求、LocalStorage、MySQL 数据库、UI 组件)。它们是干活的“员工”。
js
//❌ 订单类直接依赖了 MySQL 这个具体的底层类
class Order {
save() {
const db = new MySQL(); // 写死了 MySQL
db.insert();
}
}
//✅ Order 不管具体是什么 DB,只要外部传进来一个符合规范的对象即可
class Order {
constructor(database) {
this.db = database; // 依赖注入
}
save() {
this.db.insert(); // 面向抽象调用
}
}
const order = new Order(new MongoDB()); // 外部决定用什么DB
设计模式
- 抽象工厂模式: 客户端只依赖抽象工厂和抽象产品,具体生产什么由配置文件或外部决定
- 策略模式:上下文(Context,高层)不依赖于具体的算法实现(低层),而是依赖于统一的策略接口(抽象)
- **模板方法模式 **:父类(高层)定义流程骨架,并依赖于内部的抽象方法;子类(低层)去实现这些抽象方法。高层不依赖于具体的子类
前端应用
- 依赖流入(DI):NestJS 的依赖注入 (DI) 机制
- 单元测试
- React / Vue 中的组件设计
- React 的 Render Props / children
- Vue 的 插槽 (Slot)
6. 迪米特法则(LoD)⭐
迪米特法则 Law of Demeter 或叫 最少知识原则 Least Knowledge Principle
一个对象尽量少和其他对象交互
设计模式
- 外观模式:为子系统中的一组接口提供一个一致的门面(高层接口),客户端只需跟门面打交道。
- 中介者模式:多个类互相通信时,不让它们直接引用,而是让它们都跟一个“中介者”通信(比如 MVC 中的 Controller)。
7. 合成复用原则(CRP)
合成复用原则(CRP) Composite Reuse Principle
优先用组合,而不是继承。
设计模式
- 组合模式
- 装饰器模式
前端应用
- React hooks(组合)
- Vue composition API
- HOC
个人问题
单一职责和接口隔离区别?
- 单一职责(SRP):控制“做什么”——一个模块只做一件事,从“内部结构”拆
- 接口隔离(ISP):控制“给谁用”——不同用户用不同接口,从“对外暴露”拆
| 维度 | 单一职责(SRP) | 接口隔离(ISP) |
|---|---|---|
| 关注点 | 模块职责 | 接口粒度 |
| 解决问题 | 模块太“胖” | 接口太“臃肿” |
| 作用对象 | 类 / 模块 | 接口 / API |
| 本质 | 拆功能 | 拆接口 |
和设计模式关系
单一职责(SRP)
- 装饰器模式 👉 不改原逻辑,只增强
- 代理模式 👉 职责分离
- 外观模式 👉 外部简单,内部拆分
接口隔离(ISP)
- 适配器模式 👉 转换接口
- 策略模式 👉 提供不同接口能力
- 依赖注入(DI) 👉 按需注入