zhangdizhangdi

设计原则

介绍

设计原则是指导代码结构的思想,而设计模式是这些思想的具体实现方式。

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) 👉 按需注入