设计模式复习指南:从七大原则到代码落地的闭环方法
简介:这份《大话设计模式复习》文档面向具备一定面向对象编程基础、工作1至3年的研发人员,系统梳理设计模式的核心知识体系。内容从设计模式的定义、分类、特点与应用场景切入,详细讲解单一职责、开放-封闭、依赖倒转、里氏代换、迪米特法则、合成/聚合及接口隔离七大设计原则,并深入剖析抽象工厂、策略、外观、适配器、组合、单例、中介者七种常用模式,以及其余十五种模式,每种均配以定义、UML类图、基础代码与实例说明。文档还横向对比不同模式的差异,总结模式选择的关键维度,并附练习题与模拟试卷辅助巩固。资源包为单个docx文件,大小约3.66MB,目录结构清晰,便于按章节检索学习。目前已有191人学习,适合希望提升代码可维护性、可扩展性与复用能力,并在实际项目中灵活选用设计模式的开发者参考。
1. 从一份复习文档说起:为什么设计模式总在“面试造火箭,入职拧螺丝”
很多人对设计模式的认知,是从面试前夜翻八股文开始的。背了一堆“开闭原则”“里氏代换”,真到项目里写业务代码,还是if-else一把梭。这份《大话设计模式复习》文档的价值,不在于它又列了一遍 23 种模式的定义,而在于它把“定义—UML 类图—基础代码—实例—优缺点—违反哪条原则”串成了一条完整的复习链路,还附了模式对比表和模拟试卷。它适合工作 1-3 年、面向对象基础还在但设计经验偏弱的开发者,尤其是准备跳槽或想系统梳理 Java 设计模式的人。我拿到这份文档后,没有直接通读,而是按“七大原则 → 七大重点模式 → 剩余模式 → 对比表 → 试卷”的顺序拆了一遍,下面把拆解过程和落地方法写清楚。
2. 七大设计原则:先立规矩,再看模式怎么“守规矩”
2.1 原则不是口号,是判断模式好坏的标尺
文档开篇把七大原则放在模式之前,这个顺序是对的。很多人学模式只记结构,不记原则,结果用工厂模式写出一个“每加一个产品就要改工厂类”的代码,还觉得自己用了设计模式。实际上,抽象工厂模式在文档里被明确标注“违反开放-封闭原则”——新增产品等级结构时,抽象工厂和所有具体工厂都要改。这不是说抽象工厂不好,而是说它有明确的适用边界:产品族稳定、产品等级结构不常变时才划算。
七大原则里,最容易被误解的是里氏代换原则。文档举了“正方形是矩形的子类”这个经典反例:如果矩形有setWidth和setHeight,正方形继承后,设置宽高会互相影响,替换矩形时行为就不一致了。所以里氏代换不是“子类能编译通过就行”,而是“子类替换父类后,父类的行为契约不能被破坏”。判断方法很简单:客户端代码如果依赖父类的某个不变量(比如面积 = 宽 × 高),子类就不能悄悄改掉这个不变量。
依赖倒转原则和合成/聚合原则是模式实现的两大支柱。依赖倒转要求高层模块和低层模块都依赖抽象,文档里工厂模式的客户端依赖IUser和IDepartment接口,而不是SqlserverUser具体类,就是这条原则的落地。合成/聚合原则则解释了为什么装饰模式用组合而不是继承——继承是编译期绑定,组合是运行期绑定,装饰器可以一层层套,继承只能固定层级。
2.2 用一段代码验证“开闭原则”是否真的落地
文档里策略模式的商场收银系统实例,是验证开闭原则的好材料。正常收费、满减、打折三种策略,如果写成if (type == "正常") ... else if (type == "满减") ...,新增一种“满 1000 打 8 折”就要改if-else链,违反开闭。用策略模式改写后,新增策略只需加一个类,客户端通过Context注入即可。
// 策略接口:所有收费算法的公共契约 interface CashSuper { double acceptCash(double money); } // 正常收费策略 class CashNormal implements CashSuper { @Override public double acceptCash(double money) { return money; // 原价返回 } } // 满减策略:满 moneyCondition 减 moneyReturn class CashReturn implements CashSuper { private double moneyCondition; private double moneyReturn; public CashReturn(double moneyCondition, double moneyReturn) { this.moneyCondition = moneyCondition; this.moneyReturn = moneyReturn; } @Override public double acceptCash(double money) { // 只有达到满减门槛才减,否则原价 if (money >= moneyCondition) { return money - Math.floor(money / moneyCondition) * moneyReturn; } return money; } } // 上下文:持有策略,对外暴露统一入口 class CashContext { private CashSuper cashSuper; public CashContext(CashSuper cashSuper) { this.cashSuper = cashSuper; } public double getResult(double money) { return cashSuper.acceptCash(money); // 委托给具体策略 } } // 客户端:新增策略时只加类,不改这里 public class Client { public static void main(String[] args) { CashContext ctx = new CashContext(new CashReturn(300, 100)); System.out.println(ctx.getResult(600)); // 输出 400.0 } }这段代码的关键参数是moneyCondition和moneyReturn,分别表示满减门槛和减免金额。Math.floor(money / moneyCondition)处理的是“满 300 减 100,满 600 减 200”这种可叠加场景。如果业务规则是“每满 300 减 100,上不封顶”,这个写法就够用;如果是“满 300 减 100,但只减一次”,就要把Math.floor去掉。文档里 C++ 版本的CashReturn只做了一次判断,Java 改写时我按可叠加逻辑补了floor,这是实际项目里更常见的需求。
提示:策略模式和简单工厂结合时,
CashContext构造函数可以传入字符串类型,内部用switch创建策略。但这样CashContext就同时承担了“创建”和“执行”两个职责,违反单一职责原则。文档在对比表里也提到策略模式和状态模式容易混淆,区别在于策略模式由客户端选择算法,状态模式由状态自身驱动转移。
3. 七大重点模式:抽象工厂、单例、中介者的代码骨架与参数边界
3.1 抽象工厂:产品族扩展的代价在哪里
文档把抽象工厂放在七大模式之首,并给了可移植数据访问程序的完整实例:IFactory接口声明CreateUser()和CreateDepartment(),SqlServerFactory和AccessFactory分别创建SqlserverUser+SqlserverDepartment和AccessUser+AccessDepartment。客户端只依赖IFactory、IUser、IDepartment三个抽象,切换数据库时只改一行new SqlServerFactory()为new AccessFactory()。
这个实例的参数边界很清晰:产品族是“User + Department”,产品等级结构是“SqlServer 实现”和“Access 实现”。如果新增一个“Oracle 实现”,只需加OracleFactory、OracleUser、OracleDepartment,符合开闭原则。但如果新增一个“Login 对象”,就要改IFactory、SqlServerFactory、AccessFactory三个接口/类,违反开闭。文档在优缺点里写得很直白:“增加新产品的等级结构,需修改抽象工厂和所有具体工厂”。所以抽象工厂的选型判断就一句话:产品族稳定、产品等级结构可能扩展时用;反过来,产品等级结构稳定、产品族可能扩展时,工厂方法更合适。
// 抽象工厂接口:声明创建产品族的方法 interface IFactory { IUser createUser(); IDepartment createDepartment(); } // 具体工厂:SqlServer 产品族 class SqlServerFactory implements IFactory { @Override public IUser createUser() { return new SqlserverUser(); // 返回具体产品 } @Override public IDepartment createDepartment() { return new SqlserverDepartment(); } } // 抽象产品:User 的接口 interface IUser { void insert(User user); User getUser(int id); } // 具体产品:SqlServer 的 User 实现 class SqlserverUser implements IUser { @Override public void insert(User user) { System.out.println("在 Sqlserver 中给 User 表增加一条记录"); } @Override public User getUser(int id) { System.out.println("在 Sqlserver 中根据 ID 得到 User 表一条记录"); return new User(); } } // 客户端:只依赖抽象,切换工厂即可切换数据库 public class Client { public static void main(String[] args) { IFactory factory = new SqlServerFactory(); // 改这里即可切换 Access IUser user = factory.createUser(); user.insert(new User()); user.getUser(1); } }IFactory里的两个方法返回类型都是接口,这是依赖倒转原则的直接体现。SqlserverUser.insert里的System.out.println只是占位,实际项目里应该替换成 JDBC 或 ORM 调用。文档里User和Department类只有id、name、deptname字段和 getter/setter,这是为了聚焦模式结构,真实业务对象会有更多字段和校验逻辑。
3.2 单例模式:双重检查锁定的 volatile 不能省
文档在剩余模式分析里给了单例模式,但篇幅不长。单例的坑不在“怎么写”,而在“怎么写对”。饿汉式在类加载时就创建实例,线程安全但可能浪费资源;懒汉式加synchronized性能差;双重检查锁定(DCL)是常见折中,但instance必须加volatile,否则指令重排序可能导致其他线程拿到一个“半初始化”的对象。
public class Singleton { // volatile 禁止指令重排序,保证可见性 private static volatile Singleton instance; private Singleton() { // 私有构造,防止外部 new } public static Singleton getInstance() { if (instance == null) { // 第一次检查,避免每次加锁 synchronized (Singleton.class) { if (instance == null) { // 第二次检查,防止重复创建 instance = new Singleton(); } } } return instance; } }参数说明:volatile在这里不是可选项。instance = new Singleton()分三步:分配内存、初始化对象、将引用指向内存。没有volatile时,步骤 2 和 3 可能重排序,线程 B 在第一次检查时看到instance != null,但对象还没初始化完,直接使用就会出问题。文档里没有展开这一点,但这是单例模式最经典的“血泪经验”。
3.3 中介者模式:同事类不直接通信,中介者承担转发
文档把中介者模式放在七大模式里,和迪米特法则直接对应。迪米特法则要求“两个类若不必直接通信,则不发生直接作用”,中介者模式就是这条原则的落地:同事类只持有中介者引用,所有交互通过中介者转发。文档里的 UML 类图显示,Mediator抽象类声明Colleague之间的通信方法,ConcreteMediator持有多个ConcreteColleague,Colleague抽象类持有Mediator引用。
实际项目里,中介者模式常用于 UI 组件联动:按钮点击后,文本框、下拉框、状态栏都要更新,如果按钮直接持有其他组件引用,耦合会爆炸。用中介者后,按钮只通知中介者,中介者决定通知谁。代价是中介者可能变成“上帝类”,所以文档在对比表里把中介者和责任链放在一起对比:责任链是链式传递,中介者是星型转发,前者适合审批流,后者适合组件联动。
4. 剩余 15 种模式:对比表比逐个背定义更有用
4.1 创建型模式对比:简单工厂 vs 工厂方法 vs 抽象工厂
文档最后一部分的模式对比表,是整份资料里最值得反复看的内容。创建型模式里,简单工厂、工厂方法、抽象工厂的区别可以用一张表说清:
| 维度 | 简单工厂 | 工厂方法 | 抽象工厂 |
|---|---|---|---|
| 工厂数量 | 一个工厂类 | 每个产品一个工厂类 | 每个产品族一个工厂类 |
| 产品维度 | 单一产品等级 | 单一产品等级 | 多个产品等级(产品族) |
| 新增产品 | 改工厂类,违反开闭 | 加工厂类,符合开闭 | 改抽象工厂,违反开闭 |
| 适用场景 | 产品少且稳定 | 产品扩展频繁 | 产品族稳定、族内产品一起用 |
简单工厂的switch是“后悔药”最多的地方:产品少时写着快,产品一多就变成几百行case。工厂方法把switch拆到各个工厂类里,新增产品不影响已有代码。抽象工厂则是在工厂方法基础上加了“产品族”维度,适合数据库访问、跨平台 UI 组件这类场景。
4.2 结构型模式对比:适配器、装饰器、代理的边界
文档把适配器、装饰器、代理放在一起对比,这三个模式结构相似但意图完全不同。适配器是“接口转换”,让不兼容的接口能一起工作;装饰器是“功能增强”,在不改原类的前提下叠加功能;代理是“访问控制”,在客户端和目标对象之间加一层间接层,控制访问权限或延迟加载。
判断方法:如果客户端需要的是“另一个接口”,用适配器;如果需要“同样的接口但功能更多”,用装饰器;如果需要“同样的接口但访问受控”,用代理。文档里组合模式和装饰器模式的对比也值得注意:组合模式是“整体-部分”树形结构,装饰器是“一层套一层”的链式结构,前者用于树形菜单、文件系统,后者用于 IO 流、权限校验链。
4.3 行为型模式对比:策略 vs 状态、观察者 vs 中介者
策略模式和状态模式的类图几乎一样,区别在“谁决定切换”。策略模式由客户端选择算法,Context不关心算法怎么变;状态模式由状态自身决定下一个状态,Context只是状态转移的载体。文档里责任链模式和中介者模式的对比也很实用:责任链是“请求沿链传递,直到被处理”,中介者是“请求发给中心,由中心转发”,前者适合审批流、过滤器链,后者适合 UI 联动、聊天室。
观察者模式和中介者模式容易混:观察者是“一对多”依赖,主题通知所有观察者;中介者是“多对多”协调,同事之间不直接通信。文档在对比表里把这两组都列出来了,复习时先看对比表,再回头看具体模式的 UML 和代码,效率比从头逐个背高得多。
5. 避坑与排查:复习设计模式时最容易翻车的五个点
5.1 现象:背了 23 种模式,写代码还是if-else堆砌
原因:只记了模式结构,没记模式要解决的具体问题。文档里每个模式都有“适用场景”和“应用原则”,但复习时容易跳过。解决:每学一个模式,强制问自己“不用这个模式,代码会变成什么样”。比如策略模式,不用它时if-else链会随折扣类型增加而膨胀;用了它,新增折扣只加类。把“不用模式的坏处”写下来,比背定义管用。
5.2 现象:抽象工厂新增一个产品,所有工厂类都要改
原因:抽象工厂的固有缺陷,文档在优缺点里已经写明“增加新产品的等级结构,需修改抽象工厂和所有具体工厂”。解决:选型时先判断“产品族稳定还是产品等级结构稳定”。如果产品等级结构(如 User、Department)稳定,产品族(如 SqlServer、Access)可能扩展,用抽象工厂;如果产品等级结构本身会频繁新增,改用工厂方法或简单工厂,避免抽象工厂的“牵一发动全身”。
5.3 现象:单例模式在并发环境下创建了多个实例
原因:懒汉式没加锁,或 DCL 没加volatile。解决:按 3.2 的代码模板写,volatile和两次null检查都不能省。如果实例创建成本不高,直接用饿汉式,类加载时创建,简单且线程安全。文档里没有展开并发细节,但这是单例模式在实际项目里最容易翻车的地方。
5.4 现象:装饰器和代理分不清,代码结构写混
原因:两者都持有目标对象引用,都实现同一接口。解决:看意图。装饰器是“增强功能”,客户端主动套多层;代理是“控制访问”,客户端以为在调目标对象,实际调的是代理。文档里适配器、装饰器、代理的对比表可以直接拿来当检查清单:接口是否改变、功能是否增强、访问是否受控,三个问题问完就能选对。
5.5 现象:模式对比表背了,但面试时说不清“为什么选这个不选那个”
原因:对比表只记了结论,没记判断维度。解决:文档最后的“模式选择关键维度”是抓手——产品族是否稳定、算法是否频繁扩展、对象交互是否复杂、是否需要控制访问。每个维度对应一组模式,面试时先问清场景,再按维度排除,最后给出选型理由。比如“数据库访问组件,产品族稳定但可能换数据库”,直接对应抽象工厂;“折扣算法经常新增”,直接对应策略模式。
6. 用模拟试卷做闭环验证:一份可复现的复习节奏
文档附了练习题和模拟试卷,这是很多人忽略的部分。我的用法是:先按“七大原则 → 七大模式 → 剩余模式 → 对比表”过一遍,然后合上文档,用模拟试卷限时做一遍。做题时重点看两类题:一类是“给出场景选模式”,一类是“给出模式写 UML 或代码骨架”。前者验证选型判断,后者验证结构记忆。
如果模拟试卷里“抽象工厂和工厂方法的区别”答不全,就回到第 4.1 的对比表,把“工厂数量、产品维度、新增产品的影响”三个维度默写一遍。如果“策略模式和状态模式的区别”说不清,就回到第 4.3,用“谁决定切换”这个判断点重新推一遍。文档里的代码实例不用逐行抄,但每个模式的“基础代码”骨架要能默写出来:抽象工厂的IFactory+IUser+IDepartment,策略模式的Strategy+Context,单例的 DCL 模板。
我自己的习惯是,每复习完一个模式,就在模拟试卷对应题目旁边写一句“这个模式不用会怎样”。比如单例模式旁边写“不用会重复创建数据库连接,资源耗尽”;策略模式旁边写“不用会if-else膨胀,新增折扣要改客户端”。这些句子在面试时比背定义更管用,因为它们直接对应“为什么用”这个核心问题。
注意:文档里的 C++ 代码和 Java 代码混排,复习时建议统一用一种语言重写。Java 开发者可以把 C++ 的
CashSuper、CashReturn改写成 Java 版本,顺便验证自己是否真的理解了虚函数和接口的区别。改写过程中如果发现某个模式的结构写不出来,说明还没掌握,回到 UML 类图重新推一遍。
从那以后我每次复习设计模式,都强制走一遍“原则 → 模式 → 对比 → 试卷”的闭环,不再从头到尾通读。这份文档的价值在于它把这条闭环需要的材料都备齐了,剩下的就是动手改写和默写。希望帮到你。
本文还有配套的精品资源,点击获取