设计模式的 7 大基本原则有哪些?
结论先行:常说的"7 大原则"是 SOLID 五条(单一职责、开闭、里氏替换、接口隔离、依赖倒置)再加两条补充:迪米特法则与合成复用原则。它们的共同目标只有一个——把依赖指向抽象、把变化关进边界。原则之间会互相拉扯,所以它是权衡工具,不是考核指标。
1. 一张表过完 7 条
| 缩写 | 原则 | 一句话 | 违反时的典型信号 |
|---|---|---|---|
| SRP | 单一职责(Single Responsibility) | 一个类只有一个"变化的理由" | 类名带 And/Manager,方法两百行 |
| OCP | 开闭(Open-Closed) | 对扩展开放,对修改关闭 | 每加一个渠道就改 switch |
| LSP | 里氏替换(Liskov Substitution) | 子类必须能替换父类 | 子类实现里抛 UnsupportedOperationException |
| ISP | 接口隔离(Interface Segregation) | 接口要小而专 | 实现类里一堆空方法 |
| DIP | 依赖倒置(Dependency Inversion) | 依赖抽象,不依赖实现 | 业务代码里 new 具体实现 |
| LoD | 迪米特(Law of Demeter) | 只和直接朋友说话 | a.getB().getC().doWork() |
| CRP | 合成复用(Composite Reuse) | 优先组合,而非继承 | 为复用代码而继承,继承链四层 |
2. 依赖倒置:把箭头反过来
违反 DIP:OrderService ──new──► MysqlOrderDao(编译期焊死)
遵循 DIP:OrderService ──► OrderRepo(接口)◄──实现── MysqlOrderDao
◄── RedisOrderDao(换存储不改业务)
上层模块不再依赖下层实现,两者都依赖抽象——依赖方向被"倒置"了。
// 违反:业务类把具体存储焊死,换库要改业务
class OrderService {
private final MysqlOrderDao dao = new MysqlOrderDao();
}
// 遵循:面向接口 + 构造注入
interface OrderRepo {
void save(Order o);
Order findById(long id);
}
class OrderService {
private final OrderRepo repo; // 只认抽象
OrderService(OrderRepo repo) { // 由外部注入
this.repo = repo;
}
void place(Order o) {
repo.save(o);
}
}
Spring 的构造器注入本质上就是 DIP 的落地:容器负责 new,业务只声明"我需要一个 OrderRepo"。
3. 里氏替换:契约比继承重要
子类型要满足父类型的行为契约:前置条件不能加强、后置条件不能削弱、不抛父类没声明的受检异常。
class Rectangle {
protected int w, h;
void setW(int w) { this.w = w; }
void setH(int h) { this.h = h; }
int area() { return w * h; }
}
class Square extends Rectangle {
@Override void setW(int w) { this.w = w; this.h = w; } // 偷偷改高度
@Override void setH(int h) { this.w = h; this.h = h; }
}
// 调用方按 Rectangle 契约写:
Rectangle r = new Square();
r.setW(5); r.setH(4); // 期望 area=20,实际 16 —— 契约被破坏
判断口径很简单:使用者不知道具体子类时,行为是否仍符合预期。
4. 接口隔离:别逼实现类写空方法
// 违反 ISP:胖接口把不相干能力绑在一起,机器人也得实现 eat()
interface Worker { void work(); void eat(); }
// 遵循:按能力拆分,谁需要谁实现
interface Workable { void work(); }
interface Eatable { void eat(); }
class Robot implements Workable { public void work() { /* ... */ } }
5. 迪米特:只和直接朋友说话
对象的方法里,只应调用:自身、自己的字段、入参、自己创建的对象。典型反例是"链式导航":
// 违反:Service 顺着依赖爬了三层,任一环改结构都要跟着改
double price = order.getUser().getLevel().getDiscount().getRate() * order.getAmount();
// 遵循:让 order 自己算,内部结构对外不可见
double price = order.discountedPrice();
口诀是 Tell, Don't Ask(告诉它做什么,别问它内部长什么样)。
6. 合成复用:继承是强耦合
| 维度 | 继承(Inheritance) | 组合(Composition) |
|---|---|---|
| 绑定时机 | 编译期写死 | 运行时注入 |
| 耦合度 | 强,父类改动会波及子类(脆弱基类) | 弱,只依赖接口 |
| 可否替换 | 不能 | 随时换实现 |
| 复用粒度 | 整个类 | 按需挑选能力 |
| 典型误用 | class ArrayList01 extends ArrayList | new ArrayList<>() 作为字段 |
只有当"子类确实是父类的一种"(is-a)时才继承;只是"想复用代码"(has-a)就用组合。
7. 原则打架时怎么选
- SRP 的"职责"粒度取决于变化频率:同一个类里的两个方法如果总是一起改,就不必拆。
- DIP 会引入接口,接口多了导航成本变高;边界清晰的核心域值得,一次性脚本不值得。
- OCP 与 YAGNI 冲突时,以"变化已经发生"为准,不要为想象中的扩展预留插槽。
8. 常见追问
Q:开闭原则是不是"永远不改代码"? 不是。它说的是稳定的骨架不改,扩展靠新增。真要改的是"扩展点"本身的设计,那属于重构,不违背 OCP。
Q:7 条原则里哪条最重要? 工程上最常被违反、收益也最大的是 DIP:它直接决定单测能否替换掉数据库和外部服务。
小结
- 7 条 = SOLID 五条 + 迪米特 + 合成复用,目标都是依赖抽象、隔离变化。
- 识别违反比背定义有用:
switch越加越长、空方法一堆、new写在业务里、链式导航。 - 原则互相制约,落地时按变化频率和团队成本权衡,不要当 KPI 刷。