设计原则与模式
设计原则回答"代码之上的一层怎么组织":高内聚低耦合、依赖倒置、组合优于继承。模式是反复出现的结构的命名。本章讲 SOLID、经典模式解决的问题,以及模式的滥用。
一切设计原则的地基是两个词:
- 内聚:一个模块内部的元素为了同一个目的在一起(高内聚 = 改一个需求只动一个模块);
- 耦合:模块之间的依赖强度(低耦合 = 一个模块的实现变化不波及别的模块)。
高内聚低耦合不是口号,是变更成本的度量:需求的变更应该被局部化。判断设计好坏的标准只有一条——下一次需求变更要动几个文件。
| 原则 | 一句话 | 实质 |
|---|---|---|
| SRP 单一职责 | 一个模块只有一个变更理由 | 内聚的模块级表述 |
| OCP 开闭 | 对扩展开放、对修改关闭 | 用新增代码代替修改代码 |
| LSP 里氏替换 | 子类能无感替换父类 | 子类型的契约(行为兼容) |
| ISP 接口隔离 | 接口小而专,别强迫实现用不到的方法 | 接口面向调用者的需要 |
| DIP 依赖倒置 | 依赖抽象(接口)而非具体实现 | 低耦合的机制 |
**依赖倒置(DIP)**是低耦合的核心机制:调用方定义接口,实现方实现它——接口应面向调用者的需要,而不是实现方的分类("这个类有什么方法")。
组合优于继承:继承把父类实现细节暴露给子类(脆弱基类问题:父类改动破坏子类),组合通过委托与接口复用——面向对象的核心是动态分派而非继承(→ 编程范式),组合表达"有一个",继承只用于"是一个"的真子类型(LSP 成立的场合)。
经典模式与它们解决的问题
Section titled “经典模式与它们解决的问题”模式是反复出现的结构的命名(GoF 23 个,常用的不到十个):
| 模式 | 解决的问题 |
|---|---|
| 策略 | 一族可互换的算法(排序策略、支付方式)——运行时选择 |
| 观察者 | 一对多的通知(事件、订阅)——解耦事件源与响应者 |
| 工厂 | 创建逻辑集中(对象构造的复杂度) |
| 适配器 | 接口不匹配的胶水(旧接口接新系统) |
| 装饰器 | 不改接口加职责(中间件、I/O 包装) |
模式命名了反复出现的结构,便于沟通——"这里用观察者"一句话传达一整套设计意图。价值在词汇,不在教条。
过度设计与设计不足同样有害:
- 为未来设计("以后可能要用"):未来通常不来,来的和猜的不一样——YAGNI(You Aren't Gonna Need It);
- 模式堆砌:三层抽象调一个加法——抽象层数是成本(每层都要读);每次抽象都要付出阅读成本,收益必须大于它("两处重复才抽象"规则——Rule of Three);
- 接口过早固化:抽象错了比不抽象更贵(改接口 = 改所有实现)。
判断标准仍是变更成本:这个抽象让下一次变更便宜了吗? 没有就不加。
与语言的关系
Section titled “与语言的关系”现代语言吸收了模式的机制:
- 一等函数替代策略模式(函数即策略);
- 闭包/lambda 简化观察者与命令;
- generics/接口简化工厂与适配器。
Go 的教训(有意简化:无继承、无异常)与 Rust 的 trait——语言设计者在把"好的设计"内置化。模式的形态随语言演化,问题不变:解耦、复用、变更局部化。
- 高内聚低耦合的实质是变更成本:一次需求变更要动几个文件
- DIP 是低耦合的机制:接口应面向调用者的需要;组合优于继承
- 模式命名反复出现的结构,便于沟通;策略/观察者/适配器覆盖多数场景
- 过度设计与设计不足同样有害:每次抽象都要付阅读成本
- 判断标准:这个抽象让下一次变更便宜了吗