设计模式总览
设计模式不是为了让代码看起来“高级”,而是为了解决对象协作中反复出现的问题。学习设计模式时,不要只背模式名字,要先想清楚三个问题:
- 这段代码现在的变化点在哪里?
- 哪些对象之间耦合太紧?
- 用模式之后,是不是让扩展更容易、维护更清楚?
学习顺序
建议按下面顺序学习:
面向对象原则
-> 创建型模式:对象怎么创建
-> 结构型模式:对象怎么组合
-> 行为型模式:对象怎么协作
-> 回到业务代码中判断是否真的需要模式设计模式的本质是“把变化封装起来”。如果一个地方不会变化,强行套模式只会让代码更复杂。
七大原则
单一职责原则
一个类只负责一类明确的事情。类的职责越混乱,修改时越容易牵一发动全身。
常见坏味道:
- 一个
UserService同时处理用户注册、短信发送、Excel 导出、权限校验。 - 一个工具类越写越大,里面混着日期、文件、HTTP、加密、数据库操作。
优化思路:
注册流程 -> UserService
短信发送 -> SmsService
权限判断 -> PermissionService
报表导出 -> UserExportService单一职责不是要求“一个类只有一个方法”,而是要求一个类只有一个清晰的变化原因。
开闭原则
对扩展开放,对修改关闭。新增功能时尽量通过新增类、组合、配置来完成,而不是反复修改老代码。
典型场景:
if ("wechat".equals(type)) {
// 微信支付
} else if ("alipay".equals(type)) {
// 支付宝支付
} else if ("bank".equals(type)) {
// 银行卡支付
}当支付方式越来越多时,这段代码会越来越难维护。可以用策略模式或工厂模式把不同支付方式拆出去。
开闭原则不是绝对不修改旧代码,而是把高频变化点设计成容易扩展。
里氏替换原则
子类应该可以替换父类,并且不破坏原有程序逻辑。
如果父类方法承诺“可以支付”,子类却改成“部分情况下直接抛异常”,调用方就会被破坏。
错误示例:
class Bird {
void fly() {}
}
class Penguin extends Bird {
@Override
void fly() {
throw new UnsupportedOperationException();
}
}企鹅不是“会飞的鸟”,更合理的抽象是:
interface Bird {}
interface Flyable {
void fly();
}依赖倒置原则
高层模块不要依赖低层实现,二者都应该依赖抽象。
不要让业务代码直接绑定某个具体实现:
class OrderService {
private final MysqlOrderRepository repository = new MysqlOrderRepository();
}更好的方式是依赖接口:
interface OrderRepository {
void save(Order order);
}
class OrderService {
private final OrderRepository repository;
OrderService(OrderRepository repository) {
this.repository = repository;
}
}这样以后从 MySQL 换成 Redis、MongoDB、远程接口时,业务层不需要跟着改。
接口隔离原则
接口应该小而专一,调用方不应该被迫依赖自己用不到的方法。
坏味道:
interface Worker {
void code();
void test();
void deploy();
void designUI();
}后端开发可能不需要 designUI(),运维可能不需要 code()。可以拆成多个小接口:
interface Developer {
void code();
}
interface Tester {
void test();
}
interface Deployer {
void deploy();
}接口隔离可以减少无意义实现,也能让职责边界更清楚。
迪米特法则
一个对象应该尽量少知道其他对象的内部细节。也叫“最少知识原则”。
坏味道:
order.getUser().getAddress().getCity().getCode();调用方知道了太多对象内部结构,一旦中间结构变化,调用方也要改。可以把行为封装到对象内部:
order.getDeliveryCityCode();迪米特法则不是禁止对象协作,而是避免调用方穿透多层对象去操作内部细节。
合成复用原则
优先使用组合,而不是继承。
继承适合稳定的“是一个”关系,组合适合可变的“有一个”能力。
class OrderService {
private final DiscountPolicy discountPolicy;
private final PaymentClient paymentClient;
}组合的好处是可以替换、可以测试、可以按业务场景装配。继承层级一旦太深,扩展会变得很僵硬。
UML 关系速记
| 关系 | 含义 | Java 中常见表现 |
|---|---|---|
| 依赖 | 临时使用对方 | 方法参数、局部变量 |
| 关联 | 长期知道对方 | 成员变量 |
| 聚合 | 整体和部分,部分可独立存在 | 班级和学生 |
| 组合 | 整体和部分,生命周期强绑定 | 订单和订单明细 |
| 继承 | 子类继承父类 | extends |
| 实现 | 类实现接口 | implements |
读类图时重点看“谁持有谁”“谁调用谁”“谁负责创建谁”,不要只看箭头样式。
模式分类
创建型模式
关注对象创建,解决“对象怎么来”的问题。
结构型模式
关注对象组合,解决“对象怎么组织起来”的问题。
行为型模式
关注对象协作,解决“对象之间怎么交互”的问题。
选择模式的判断方法
可以按下面的方式判断:
| 代码问题 | 优先考虑 |
|---|---|
| 创建对象逻辑复杂、到处 new | 工厂、建造者 |
| 系统只需要一个共享实例 | 单例 |
| 需要复制复杂对象 | 原型 |
| 老接口和新接口不兼容 | 适配器 |
| 多个维度独立变化 | 桥接 |
| 不改原类增加功能 | 装饰器、代理 |
| 树形结构统一处理 | 组合 |
| 子系统太复杂,需要统一入口 | 外观 |
| 大量重复小对象 | 享元 |
| 多个处理器按顺序处理请求 | 责任链 |
| if-else 选择不同算法 | 策略 |
| 状态变化导致行为变化 | 状态 |
| 固定流程中部分步骤变化 | 模板方法 |
| 一对多通知 | 观察者 |
学习建议
- 先理解场景,再看代码。
- 先写普通实现,再重构成模式。
- 模式不是越多越好,能让代码更清楚才有价值。
- 在 Spring、JDK、MyBatis 里找模式,比单独背示例更有效。