设计原则与分类
开篇:为什么要学设计模式?
很多同学一听到"设计模式"四个字就头大——23 种模式,每种又有好几个角色,背都背不完,学来有啥用?
先别急着背。换个角度想:你去宜家买家具,说明书上画的那些"先插 A 孔再拧 B 螺丝"的步骤图,就是一种"模式"。它不是让你死记硬背,而是前人踩了无数次坑之后,总结出来的最省力的组装方式。
设计模式也一样。它不是语法,不是 API,而是一套解决特定问题的经验套路。当你的代码出现以下症状时,设计模式就是那张"说明书":
- 改一个需求要动十几个文件 → 你需要了解开闭原则
- 一个类又管数据库又管发短信又管日志 → 你需要了解单一职责原则
- 到处都是
if-else在判断类型 → 你需要了解策略模式 - 创建对象的代码复制粘贴了五遍 → 你需要了解工厂模式
所以,学设计模式的正确姿势是:先认识问题,再学解法。本篇先把"道"讲清楚——设计原则和分类体系,后面几篇再逐个拆解每种模式。
还有一点很重要:设计模式也是一种通用语言。当你跟同事说"这里用个策略模式吧",对方马上就能 get 到你的意思——用一个接口抽象出多种算法,然后通过注入的方式切换实现。这种沟通效率是很高的。
一、SOLID 五大原则
SOLID 是面向对象设计中最核心的五条原则,每条原则的首字母合在一起就是 SOLID。它们是所有设计模式的理论基础。
1. 单一职责原则(Single Responsibility Principle)
一句话:一个类只干一件事,只有一个引起它变化的原因。
生活类比:厨师就做菜,服务员就上菜,收银员就收钱。如果让厨师又做菜又上菜又收银,忙不过来不说,哪个环节出了问题都得找他。
// 违反 SRP:一个类又管订单又管发邮件
public class OrderService {
public void createOrder() { /* 创建订单逻辑 */ }
public void sendEmail() { /* 发邮件通知逻辑 */ }
}
// 遵守 SRP:各管各的
public class OrderService {
public void createOrder() { /* 创建订单逻辑 */ }
}
public class EmailService {
public void sendEmail() { /* 发邮件通知逻辑 */ }
}为什么重要? 当邮件模板要改的时候,你只需要动 EmailService,完全不用碰订单逻辑,也不用担心改出 bug。
2. 开闭原则(Open/Closed Principle)
一句话:对扩展开放,对修改关闭——通过新增类来扩展功能,而不是修改已有的代码。
生活类比:你的手机想加个保护壳,直接套上去就行,不需要把手机拆开重新焊电路板。
// 违反 OCP:每加一种形状就要改这个方法
public double area(Object shape) {
if (shape instanceof Circle) {
return Math.PI * ((Circle) shape).radius * ((Circle) shape).radius;
} else if (shape instanceof Rectangle) {
return ((Rectangle) shape).width * ((Rectangle) shape).height;
}
// 新增三角形?又要改这里...
return 0;
}
// 遵守 OCP:定义接口,新增形状只需加类
public interface Shape {
double area();
}
public class Triangle implements Shape {
private double base, height;
public double area() { return 0.5 * base * height; }
}关键收益:已有的代码经过测试是稳定的,新增类不会影响老逻辑,回归测试的范围大大缩小。
3. 里氏替换原则(Liskov Substitution Principle)
一句话:子类能完全替代父类出现的地方,且不产生意外行为。
生活类比:你叫了一辆出租车,来的是燃油车还是电动车都无所谓,只要能把你送到目的地就行。但如果来了一辆只能在赛道上开的 F1 赛车,那就不对了——它虽然是"车的子类",但不能替代日常出行的场景。
经典反例:正方形继承长方形。setWidth() 方法把宽改了但高不变,对长方形没问题,但正方形就"不方了"。
4. 接口隔离原则(Interface Segregation Principle)
一句话:不要让一个类被迫依赖它用不到的方法。接口应该小而专注。
生活类比:你去餐厅只想吃饭,结果菜单上还有 KTV 点歌、洗车、理发的服务——接口太"胖"了。应该拆成"餐饮菜单""娱乐菜单"等小接口。
// 违反 ISP:一个大而全的接口
public interface Worker {
void work();
void eat(); // 机器人不需要吃饭!
}
// 遵守 ISP:拆成小接口
public interface Workable { void work(); }
public interface Eatable { void eat(); }
// 人类实现两个接口,机器人只实现 Workable
public class HumanWorker implements Workable, Eatable { /* ... */ }
public class RobotWorker implements Workable { /* ... */ }5. 依赖倒置原则(Dependency Inversion Principle)
一句话:高层模块不要直接依赖低层模块,两者都应该依赖抽象(接口)。
生活类比:你的台灯插头是标准两孔插头,不管墙上的插座是公牛还是西门子的,都能插上去用。"国标插头"就是那个抽象接口,台灯和插座都依赖它,而不是台灯直接焊死在某个品牌的插座上。
// 违反 DIP:Service 直接依赖具体实现
public class UserService {
private MySQLUserDao dao = new MySQLUserDao(); // 换 Oracle 怎么办?
}
// 遵守 DIP:依赖接口,具体实现由外部注入
public class UserService {
private UserDao dao;
public UserService(UserDao dao) { this.dao = dao; }
}在 Spring 中,@Autowired 就是依赖倒置 + 依赖注入的最佳实践。你只声明接口类型,Spring 容器负责注入具体实现。
二、另外两大原则
除了 SOLID 五原则,还有两条在设计模式中频繁出现的原则:
6. 迪米特法则(Law of Demeter,最少知道原则)
一句话:一个对象应该对其他对象了解得越少越好。不要跟"陌生人"说话。
生活类比:你去政务大厅办事,只需要跟窗口工作人员说你要办什么业务。你不需要知道后面是哪个科室在处理、用的什么系统、数据存在哪台服务器上。窗口就是你唯一的"朋友"。
实际意义:如果你写出了 a.getB().getC().doSomething() 这样的链式调用,说明 A 对象知道了太多关于 B 和 C 的内部结构。一旦 C 的接口变了,A 也得跟着改。正确做法是让 A 只跟 B 交互,需要什么功能让 B 提供一个封装好的方法。
7. 合成复用原则(Composite Reuse Principle)
一句话:优先用组合(has-a),而不是继承(is-a)来实现代码复用。
生活类比:你想给自行车加个车筐,是焊死在车架上好,还是用卡扣挂上去好?显然后者更灵活——坏了能换,不要了能拆,换个款式也方便。继承就像焊死,组合就像卡扣。
// 继承方式:强耦合,改一处就要改一串
public class ElectricCar extends Car { /* ... */ }
// 组合方式:灵活可替换
public class Car {
private Engine engine; // 可以是汽油引擎,也可以是电动引擎
public Car(Engine engine) { this.engine = engine; }
}三、七大原则速记表
| 原则 | 核心思想 | 一句话记忆 |
|---|---|---|
| 单一职责 | 一个类一个职责 | 厨师只做菜 |
| 开闭原则 | 扩展开放,修改关闭 | 套手机壳,不拆手机 |
| 里氏替换 | 子类能替代父类 | 电动出租车也是出租车 |
| 接口隔离 | 接口要小而专注 | 餐厅菜单不卖理发 |
| 依赖倒置 | 面向接口编程 | 国标插头,百搭插座 |
| 迪米特法则 | 最少知道 | 政务窗口办事 |
| 合成复用 | 组合优于继承 | 车筐用卡扣,别焊死 |
四、设计模式三大分类
了解了原则之后,再来看 23 种经典设计模式。它们按照解决的问题类型,分成三大类:
用做饭来类比这三大类:
- 创建型模式 → 准备食材阶段:怎样高效地获取食材?是自己种(new),还是去超市买(工厂),还是直接叫外卖(单例直接拿)?
- 结构型模式 → 搭配食材阶段:怎样把不同的食材组合在一起?适配器就像一个转换插头,把不兼容的接口连起来;装饰器就像往菜上撒调料,不改变菜本身。
- 行为型模式 → 烹饪流程阶段:怎样协调各个步骤?模板方法定义了"先热油、再放菜、最后调味"的流程骨架;策略模式让你在"红烧"和"清蒸"之间随时切换。
下面是 23 种模式的速查表:
| 分类 | 模式名 | 一句话描述 | 典型应用 |
|---|---|---|---|
| 创建型 | 单例 | 全局只有一个实例 | Spring Bean 默认作用域 |
| 创建型 | 工厂方法 | 子类决定创建哪种产品 | LoggerFactory.getLogger() |
| 创建型 | 抽象工厂 | 创建一族相关产品 | 跨数据库的 DAO 层 |
| 创建型 | 建造者 | 分步构建复杂对象 | StringBuilder、Lombok @Builder |
| 创建型 | 原型 | 通过克隆快速创建对象 | Object.clone()、缓存拷贝 |
| 结构型 | 代理 | 为对象提供替身以控制访问 | Spring AOP、JDK 动态代理 |
| 结构型 | 适配器 | 让不兼容的接口协同工作 | InputStreamReader |
| 结构型 | 装饰器 | 动态给对象增加职责 | Java IO 流的层层包装 |
| 结构型 | 外观 | 为复杂子系统提供统一入口 | SLF4J 日志门面 |
| 结构型 | 桥接 | 抽象与实现独立变化 | JDBC 驱动体系 |
| 结构型 | 组合 | 树形结构的统一操作 | 文件夹与文件 |
| 结构型 | 享元 | 共享细粒度对象节省内存 | String 常量池、Integer 缓存 |
| 行为型 | 策略 | 算法家族,可随时替换 | 支付方式选择 |
| 行为型 | 观察者 | 状态变化时一对多通知 | 事件监听、消息订阅 |
| 行为型 | 模板方法 | 固定骨架,子类填细节 | AbstractList、JdbcTemplate |
| 行为型 | 责任链 | 请求沿链传递 | Servlet Filter、Spring Interceptor |
| 行为型 | 状态 | 状态驱动行为变化 | 订单状态机 |
| 行为型 | 命令 | 请求封装成对象 | 撤销/重做、任务队列 |
| 行为型 | 中介者 | 集中管理对象间交互 | MVC 中的 Controller |
| 行为型 | 迭代器 | 提供统一遍历接口 | Java Iterator |
| 行为型 | 访问者 | 不改结构,新增操作 | ASM 字节码框架 |
| 行为型 | 备忘录 | 保存和恢复对象状态 | 游戏存档、事务回滚 |
| 行为型 | 解释器 | 定义语法并解释执行 | 正则表达式引擎、EL 表达式 |
五、如何选择合适的设计模式?
面对一个具体问题,不用把 23 种模式都过一遍。你可以按"三步法"快速定位:
第一步:识别问题类型
- 创建对象的代码太复杂、太重复?→ 看创建型
- 类之间的关系太混乱、耦合太紧?→ 看结构型
- 对象的协作流程太僵硬、扩展困难?→ 看行为型
第二步:用关键问题缩小范围
- 需要控制实例数量?→ 单例
- 需要根据类型创建不同对象?→ 工厂
- 需要动态增加功能而不改原有类?→ 装饰器
- 需要让不兼容的接口协同工作?→ 适配器
- 需要解耦请求发送者和处理者?→ 责任链
- 需要在多种算法间灵活切换?→ 策略
第三步:验证选择是否过度设计
画一下类图,看看引入模式后类的数量和复杂度是否在可接受范围内。如果一个简单的 if-else 就能搞定的事(而且未来也不会扩展),硬套策略模式反而是过度设计。
记住一个原则:设计模式是用来解决问题的,不是用来炫技的。代码的第一要务是"能跑、好读、好改",设计模式只是达成这个目标的手段之一。
小结
本篇建立了设计模式的全局认知框架:
- 七大原则是地基 —— 它们告诉你什么样的代码是"好代码":职责单一、对扩展开放、面向接口、最少知道、组合优于继承。
- 三大分类是地图 —— 23 种模式按"创建、结构、行为"分组,各有所长,按问题类型对号入座。
- 三步选择法是指南针 —— 识别问题类型 → 关键问题缩小范围 → 验证是否过度设计。
接下来的几篇文章,我们将按照"创建型 → 结构型 → 行为型"的顺序,逐一深入拆解。每种模式都会给出生活类比、完整代码示例和面试高频考点。目标只有一个:理解本质,而不只是记住名字。