☰
设计模式复习指南:从七大原则到23种模式的代码实战与避坑
2026/10/15 9:27:59 网站建设 项目流程

简介:面向具备一定编程基础、熟悉面向对象设计的研发人员,尤其适合工作1~3年的程序员,这份设计模式复习文档聚焦软件工程中的经典模式应用。内容先厘清设计模式的定义、分类、特点与应用场景,再详解单一职责、开放封闭、依赖倒转、里氏代换、迪米特等七大设计原则;随后深入拆解抽象工厂、策略、外观、适配器、组合、单例、中介者七种常用模式,每种均配合UML类图、基础代码和实际应用实例,便于理解适用场景与实现细节。文档还覆盖其余十五种模式的要点梳理,并通过模式间对比总结选型关键维度,帮助开发者在项目中提升代码的可维护性、可扩展性与复用性。资源包仅包含一个docx格式文档,大小3.66MB,已有189人学习,适合用于系统复习、动手实践和考前自测。

1. 设计模式复习从哪下手:先建地图再背代码,别把记忆负担当成理解负担

设计模式是软件工程里被讨论最多的面向对象编程经验总结,但很多人复习设计模式时翻车,不是因为记不住 23 个模式的代码,而是面对一个具体业务场景不知道该选哪个,或者写出来的类被同事反问“这不是硬套模式吗”。这份复习文档做得比较实在的地方,是把七大设计原则、23 种经典模式、易混淆对比和模拟题串成了一条线,每个模式都配了 UML 类图、基础代码和实例。它适合工作 1-3 年的 Java 开发者,用来系统过一遍模式应用的核心判断:什么时候用、怎么落地、代价在哪。下文我会按这份文档的复习顺序,把原则、代码、对比和踩坑串起来讲。

2. 七大设计原则是模式的地基:单一职责、开放封闭与抽象边界的取舍

2.1 单一职责与开放-封闭:改动范围越小,模式价值越明显

文档把单一职责原则(SRP)放在第一位是有道理的:一个类应该只有一个引起它变化的原因。换句话说,每个类只负责一个业务维度,避免把多个变化源绑死在同一个类里。实际编码中最常见的症状是“一个类里塞了多个业务动词”——比如同时负责数据校验、金额计算和结果格式化,任何一块逻辑调整都要动这个类,测试和回归的代价随之变大。

看一个典型反例:

// 反例:一个类同时负责校验、计算和格式化 public class OrderCalculator { public double calculate(String type, double price, int count) { if (!validate(price, count)) { throw new IllegalArgumentException("参数不合法"); } double result = 0; if ("normal".equals(type)) { result = price * count; } else if ("discount".equals(type)) { result = price * count * 0.8; } return format(result); } private boolean validate(double price, int count) { return price > 0 && count > 0; } private double format(double value) { return Math.round(value * 100) / 100.0; } }

这段代码的问题是:校验规则改了要改这个类,折扣规则改了要改这个类,格式化规则改了还要改这个类。三个变化原因被耦合在一个类里,测试时也难单独覆盖。重构方向是拆成校验器、计价策略、格式化器三部分,其中计价部分正好交给策略模式承接。price和count作为入参,type作为策略选择标识,拆分后每个类只需要面向一个变化点。

开放-封闭原则(OCP)对模式的约束更关键:软件实体应该可以扩展但不修改。策略模式“新增算法只新增一个策略类、不改策略接口”是 OCP 的正面体现;抽象工厂的缺点恰恰是违反 OCP——新增一个产品等级结构时,抽象工厂接口和所有具体工厂都要跟着改。面试被问到“这个模式有什么缺点”时,答案往往藏在“它违反了哪个原则”里,文档在这方面标注得很清楚。

2.2 依赖倒转、里氏代换与接口隔离:抽象边界的三个约束

依赖倒转原则(DIP)的落地只看一句话:客户端引用抽象,不引用具体。为什么工厂模式值得用?因为客户端代码里不出现new SqlserverUser()这种具体类创建,而是factory.createUser(),具体创建逻辑被隔离在工厂内部。这样数据库从 SQLA 换成 SQLB,客户端代码一行都不用改。DIP 是很多创建型模式的根基,文档里抽象工厂实例的 Client 类只面向IFactory、IUser、IDepartment三个抽象,就是这个原因。

里氏代换原则(LSP)是继承的安全底线:子类型必须能替换父类型,且不破坏行为契约。经典的“正方形继承矩形”问题——矩形有setWidth和setHeight,正方形继承后必须同时改两个值才能保证边长一致,这已经破坏了父类的行为契约。实际代码里 LSP 出问题,通常表现是子类重写父类方法时改变了语义:父类方法返回null表示查不到,子类却抛异常,调用方按父类约定写代码就会翻车。

接口隔离原则(ISP)在文档里的例子是把“动物”接口拆成“飞行动物”“陆地动物”。实际编码中更常见的问题是:一个接口塞了太多方法,实现类被迫写一堆空实现。比如一个ReportService接口同时声明了generatePdf()、generateExcel()、generateCsv(),某类只需要 PDF,但也要实现另外两个空方法。拆成三个单一接口后,实现类只需要实现自己关心的能力。抽象工厂里的产品接口也遵循这个原则,每个产品接口只声明自己的操作。

2.3 迪米特法则与合成/聚合原则:降低耦合的两条路径

迪米特法则(LOD)的核心是“两个类若不必直接通信,就不发生直接作用,通过第三者转发”。中介者模式是 LOD 的典型应用:同事类之间不直接引用,而是通过中介者转发调用。但注意,LOD 不是无条件遵守——当同事类只有两三个、交互也简单时,强行引入中介者反而多了一层黑匣子,排错时要多跳一层。文档里中介者模式的例子也是在同事类数量多、交互关系乱的场景下才成立的。

合成/聚合原则(CARP)是比继承更灵活的复用方式:尽量使用合成(整体-部分)或聚合(关联关系),而不是继承。装饰模式是教科书级的例子——通过组合装饰器与组件来扩展功能,而不是用继承链一层层叠加子类。为什么组合优于继承?继承是编译期确定的,组合是运行期可变的;一个类只要持有组件引用,想加哪种装饰就加哪种,不需要预判所有子类组合。

提示:七大原则不是孤立的存在。文档在每种模式后面都标了“应用了哪些原则、违反了哪些原则”,复习时要重点看这个标注。面试官问模式缺点时,直接答“它违反了开放-封闭原则,因为新增 XX 类型要改 XX 处”,比说“性能不好”这种空话有说服力得多。

3. 核心模式代码拆解:抽象工厂、策略、单例、中介者怎么落地

3.1 抽象工厂模式:产品族创建接口与跨数据库访问实例

抽象工厂的适用场景是“多个产品必须配套使用”。文档里的例子是数据访问程序:用户表和部门表是同一产品族,SQLA 数据库和 SQLB 数据库是两个具体工厂。客户端不关心当前是哪个数据库,只面向接口操作。和普通工厂方法模式的区别在于:抽象工厂创建的是“一组”相关对象,工厂方法创建的只是“一类”对象。

基础结构分四层,先看接口定义:

// 抽象产品:用户表操作接口 public interface IUser { void insert(User user); User getUser(int id); } // 抽象产品:部门表操作接口 public interface IDepartment { void insert(Department dept); Department getDepartment(int id); } // 抽象工厂:声明创建产品族的方法,返回抽象产品 public interface IFactory { IUser createUser(); IDepartment createDepartment(); } // 具体工厂:SQLA 数据库实现 public class SQLAFactory implements IFactory { @Override public IUser createUser() { return new SQLAUser(); } @Override public IDepartment createDepartment() { return new SQLADepartment(); } }

注意IFactory的返回值必须是抽象产品接口,不能是具体类,否则依赖倒转就失效了。createUser()和createDepartment()成对出现,这是抽象工厂和工厂方法的本质区别——前者一次声明一个产品族的创建入口,后者只声明单一产品的创建入口。SQLB 数据库对应SQLBFactory,内部返回SQLBUser、SQLBDepartment,结构完全对称。

客户端代码只依赖抽象层:

public class Client { public static void main(String[] args) { IFactory factory = new SQLAFactory(); IUser iu = factory.createUser(); iu.insert(new User(1, "A同学")); IDepartment id = factory.createDepartment(); id.insert(new Department(1, "研发部")); // 切换数据库时只改这一行 // IFactory factory = new SQLBFactory(); } }

这里切换数据库只改new SQLAFactory()一行,后续iu、id的调用全部不用动。User和Department是简单的数据对象,只含id、name、deptname字段和 getter/setter。文档强调的一个缺点是:抽象工厂增加“新的产品等级结构”会违反 OCP。比如现在要加“日志表”操作,IFactory接口要加createLog()方法,SQLAFactory、SQLBFactory 全部要同步实现。如果业务里产品等级结构频繁增加,抽象工厂就不是好选择,可以退到工厂方法或注册式工厂。

3.2 策略模式:算法封装、上下文委托与工厂配套

策略模式解决的是“同一业务有多种算法,且算法相互可替换”的问题。文档的类划分原则写得很到位:类的划分是为了封装,分类的基础是抽象。商场收银系统的正常收费、满减、打折,就是三个具体策略。

核心代码:

// 策略接口:所有算法的公共接口 public interface Discounter { double acceptCash(double money); } // 具体策略A:正常收费 public class NormalDiscounter implements Discounter { @Override public double acceptCash(double money) { return money; } } // 具体策略B:满减,满 condition 元减 returnMoney 元 public class ReturnDiscounter implements Discounter { private final double condition; private final double returnMoney; public ReturnDiscounter(double condition, double returnMoney) { this.condition = condition; this.returnMoney = returnMoney; } @Override public double acceptCash(double money) { if (money >= condition) { return money - Math.floor(money / condition) * returnMoney; } return money; } } // 上下文:持有策略并委托执行 public class CashContext { private Discounter discounter; public void setDiscounter(Discounter d) { this.discounter = d; } public double execute(double money) { return discounter.acceptCash(money); } }

策略模式的关键在CashContext:它不关心具体策略是谁,只负责持有策略引用并委托执行。调用的写法是context.setDiscounter(new ReturnDiscounter(300, 50)),然后context.execute(299)返回 299,context.execute(600)返回 500。Math.floor(money / condition)处理了多档满减——满 300 减 50,满 600 减 100;如果场景是“一次性满减”而不是“每满”,这个公式要改成money - returnMoney而不是按倍数扣,这是满减逻辑里最容易理解错的地方。

这里有个容易忽略的边界:策略模式本身不消灭客户端的选择逻辑。如果客户端用if-else判断折扣类型再new对应策略,判断逻辑还是散落在调用方。严格按照“策略 + 简单工厂”的组合,把“根据类型创建策略”收拢到工厂类里,客户端只传一个字符串或枚举,这也是文档题目里常见的变形题。

3.3 单例、外观、适配器、组合、中介者的骨架与适用信号

单例模式是面试出现频率最高的一个,重点在并发安全。双重检查锁必须配合volatile:

public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

volatile的作用是禁止指令重排:new Singleton()在底层不是原子操作,如果不用volatile,线程 A 创建实例的过程中线程 B 可能拿到一个未完成构造的对象。这是单例模式最大的坑,后面避坑章节会展开。

外观模式的特征是给一组接口提供一个统一门面。比如一个视频播放系统有音频解码、视频解码、字幕加载三个子系统,客户端只想调playerFacade.play(file),不需要知道解码顺序。适用信号是:子系统接口多、客户端调用链长、你希望暴露一个简化入口。

适配器模式解决接口不兼容:客户端依赖Target接口,已有实现类没有实现这个接口,中间加适配器转换。对象适配器持有被适配对象引用做转发,比类适配器用继承的方式更灵活,因为适配器不绑定具体父类。

组合模式处理树形结构:文件和文件夹、部门和子部门。关键设计是组件接口同时声明叶子节点和容器节点的操作,叶子节点的add方法抛异常或空实现,容器节点用List保存子节点。适用信号是“整体-部分”递归结构。

中介者模式是迪米特法则的极端应用:多个同事类不直接引用,通过中介者转发消息。代价是中介者本身容易变成上帝类,只在交互关系复杂时才值得引入,两三个对象互相调用时硬套中介者反而是过度设计。

4. 剩余 15 种模式的快速记忆:按创建、结构、行为三类建模式地图

4.1 创建型:简单工厂、工厂方法、建造者、原型

简单工厂不属于 GoF 23 种模式,但面试一定会问。它把new具体类的逻辑收拢到一个工厂类里,客户端传类型参数。缺点是加新产品要改工厂的 switch,违反 OCP。它和工厂方法不是替代关系,是“把选择集中在一处”和“把选择下沉到子类”两种取向。

工厂方法把创建对象推迟到子类:抽象类定义业务骨架,子类决定创建哪个具体产品。适合“核心流程稳定、产品类型扩展频繁”的场景,比如报表系统支持文件、数据库、外部接口三种数据源,新增数据源时只需要新增一个子类工厂,不用动业务逻辑。

建造者模式适合“对象构造过程复杂且稳定,但各部分的具体实现可变”。典型例子是游戏角色创建:性别、发型、服装、武器各有多种选择,构建流程固定,具体选什么由不同 Builder 决定。它和工厂模式的区别:工厂关注“产出什么类型”,建造者关注“按什么步骤组装”。

原型模式通过克隆生成新对象,Java 里实现Cloneable接口并重写clone()。坑在拷贝深度:浅拷贝只复制引用,多个对象共享同一个内部组件;深拷贝要自己逐层实现克隆逻辑。

4.2 结构型:装饰、代理、桥接、享元、外观、适配器、组合

装饰模式是“在不让类爆炸的前提下加职责”。看一个简短结构:

public interface Component { void operation(); } public class ConcreteComponent implements Component { @Override public void operation() { System.out.println("具体组件操作"); } } public abstract class Decorator implements Component { protected Component component; public void setComponent(Component c) { this.component = c; } @Override public void operation() { if (component != null) { component.operation(); } } } public class LogDecorator extends Decorator { @Override public void operation() { super.operation(); System.out.println("打印操作日志"); } }

组合顺序是new LogDecorator()后setComponent(new ConcreteComponent()),调用operation()时先执行组件逻辑,再追加日志行为。注意Decorator持有的是组件接口,不是具体实现类,这样才能任意嵌套多层装饰。组合模式 vs 装饰模式的对比在这份文档里值得单独背:组合模式强调树形整体-部分结构,装饰模式强调层层包装增强行为,虽然代码里都用了递归结构,但意图完全不同。

代理模式是“控制访问”:远程代理、虚拟代理、保护代理,核心是代理类和真实类实现同一接口,客户端只认识代理。它和装饰模式的代码长得像,但代理的职责是“决定能不能调”,装饰的职责是“调完之后还要做什么”,评审代码时看类里有没有权限校验、懒加载、远程转发这类逻辑就能分辨。

桥接模式解决“两个维度同时变化”。一个经典反面教材是用继承实现颜色和形状:红色圆形、红色方形、蓝色圆形、蓝色方形,加一个颜色或形状类数量就翻倍。桥接把两个维度拆成两条独立继承链,中间用组合关联,复杂度从 M×N 降为 M+N。享元模式通过共享减少对象数量,文本编辑器里只建一套字符对象、共享相同内容,是典型的适用场景。

4.3 行为型:模板方法、观察者、命令、状态、备忘录、责任链、访问者

模板方法模式定义算法骨架,把可变步骤推迟到子类:

public abstract class DataProcessor { // 模板方法:固定流程,子类不能重写 public final void process() { read(); validate(); save(); } protected abstract void read(); protected abstract void save(); private void validate() { // 非空校验、类型校验等通用逻辑 System.out.println("执行通用校验"); } }

子类只需实现read()和save(),校验逻辑统一放在模板方法里。注意process()用final修饰,防止子类篡改流程骨架。它和策略模式的区别:模板方法控制的是“骨架”,子类只能填可变步骤;策略控制的是“整个算法”,客户端直接选择替换。

观察者模式是“发布-订阅”。它的坑在于观察者没有正确注销——主题持有观察者引用,观察者销毁后主题列表里还留着旧引用,GC 无法回收,内存慢慢涨满。命令模式把“请求”封装成对象,实现撤销重做的思路就是维护一个命令历史列表。状态模式是策略模式的亲戚,但状态对象是“可转移”的,订单从待支付到已支付到已发货是状态机,不是平级算法选择。备忘录模式保存状态快照以便回滚,恢复时要新建对象或逐个字段恢复,不能直接改原对象。责任链模式把多个处理者连成链,报销审批从组长到经理到总监就是典型链路。访问者模式把操作从元素中分离出来,数据结构稳定但操作频繁增加时才值得用,实际项目出现频率较低,面试不作重点。

文档第四部分的模式对比表,建议直接背结论:策略 vs 状态看状态是否转移;观察者 vs 中介者看广播还是中枢转发;责任链 vs 中介者看请求单向流动还是网状交互;单例 vs 原型看进程内唯一还是按需克隆;模板方法 vs 策略看控制流程还是替换算法。

5. 避坑与常见问题:模式应用中最容易翻车的五个场景

5.1 单例的双重检查锁没加 volatile

现象:高并发下单例偶尔返回多个不同实例,程序出现资源竞争问题,排查时发现getInstance()返回了不同对象。

原因:new Singleton()在 JVM 层面不是原子操作,分配内存、初始化对象、赋值引用三步可能被重排。线程 A 先分配了内存但还没初始化完成,线程 B 看到instance非空直接返回,拿到一个未构造完的对象。

解决:instance字段加volatile修饰,禁止指令重排。或者用静态内部类持有单例,由 JVM 类加载机制保证初始化一次,代码更简洁。血泪经验是:很多人记住了双重检查,却漏了volatile,评审时一问“为什么加 volatile”就卡住。

5.2 策略模式把选择逻辑散落在客户端

现象:虽然用了策略接口,但调用处写满了if ("normal".equals(type))之类的分支,新增策略时要改多处调用代码。

原因:只用了策略模式,没有配套工厂类封装“根据类型创建策略”的逻辑。策略模式解决的是算法互换,不解决算法选择,选择逻辑还是散落的。

解决:维护一个策略工厂,接收字符串或枚举,返回对应策略。客户端只依赖工厂和策略接口,分支逻辑收拢到工厂一处,新增策略时只改工厂。

5.3 抽象工厂加新产品类型,所有实现类同步改

现象:业务要新增操作日志表,结果IFactory加方法后,所有具体工厂类全部编译报错。

原因:产品等级结构的增量本质上是抽象工厂接口的扩展,扩展接口会强制所有实现类同步修改,违背开放-封闭原则。

解决:先判断业务的变化方向。如果产品族内部的产品类型相对固定,抽象工厂合适;如果产品类型频繁增加,改用工厂方法模式,或引入注册式工厂通过反射动态获取创建函数。选型时先问自己:以后是“加一个产品族”还是“加一类产品”?前者用抽象工厂,后者用工厂方法。

5.4 观察者没有注销导致内存泄漏

现象:某个模块反复打开关闭,内存占用持续上涨,GC 后也不回落,最后 OOM。

原因:主题对象持有观察者引用,观察者已经被业务层释放,但主题的监听器列表里还留着旧引用。每次重新打开模块都新建观察者并注册,旧实例无法回收。

解决:观察者注册时保存unregister句柄,或者观察者销毁时显式调用主题的移除方法。代码审查时重点检查addListener有没有成对的removeListener,我每次提交前都会全局搜一遍这两处调用是否对称。

5.5 适配器、装饰器、代理分不清

现象:代码里一个类叫XxxAdapter,但做的事是增强原类功能;另一处XxxProxy却在做接口转换。

原因:三种模式代码结构都是“包一层”,只记类名后缀没用,要记意图。适配器是接口转换——客户端要 A 接口,手里只有 B 接口;装饰器是增强——A 接口的原有行为不够,要加新行为;代理是控制访问——决定调用方能不能调 A 接口。

解决:面试和评审时直接说意图,不要说类名。比如“登录接口想复用现有 SDK 的认证方法,但参数对不上,加了一个适配层”,这是适配器;“想给日志输出自动加时间戳,但不想改原来的 Logger 类”,这是装饰器;“远程服务调用要经过网络层,客户端不直接连服务端”,这是代理。意图一出口,模式归属就很清楚。

6. 模式选择的关键维度:用“变化点”做判断,而不是靠名字猜

用这份文档复习到最后,真正能留在工作里的不是 23 个模式的名字,而是一套判断框架。我常用的切分维度是“变化点在哪里”:对象怎么创建选创建型,类怎么组合选结构型,行为怎么交互选行为型。

场景化的选择逻辑可以浓缩成一张判断表:

变化点候选模式关键判断依据
创建哪种产品/产品族简单工厂、工厂方法、抽象工厂一类对象选工厂;一组相关对象选抽象工厂;创建逻辑下沉选工厂方法
给已有类动态加职责装饰器、代理加功能选装饰器;控制访问选代理
接口不兼容适配器已有实现改不了,客户端接口固定,直接包一层转换
算法可互换策略算法平级且互不依赖,运行时可切换
流程骨架固定、步骤可变模板方法多个类共享同一流程,只有个别步骤不同
一对多通知观察者状态变化要多方响应
多对象复杂交互中介者同事类多且交互乱,才值得引入中枢
树形整体-部分组合能找到递归的容器/叶子结构
状态驱动行为状态模式行为随状态转移而变化

用这张表去套文档题目里的场景题会发现,大部分题考的不是模式本身的代码,而是“为什么在这个场景选它不选另一个”。比如“一个报表系统,数据源可能是文件、数据库、外部接口,希望新增数据源时不用改核心逻辑”——变化点是创建哪种数据源对象,答案是工厂方法而不是适配器,因为适配器解决接口不兼容,不是创建选择。

我第一次刷这份文档时犯过一个错:把每种模式的代码都背下来了,但面对“银行转账要支持短信、邮件、App 推送三种通知,且通知方式可扩展”这个场景,第一反应是观察者模式,结果是策略模式——因为通知方式之间是平级选择关系,每次选一种,不是状态变化触发的广播。从那以后,我每次做模式选型都强制先问自己三个问题:这个场景的变化点是什么?变化是新增类型还是替换行为?客户端希望感知到多少细节?三个问题问完,选型基本不会偏。文档里的练习题和模拟卷建议先刷一遍再回看对比表,正确率会比直接背代码明显高一截。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询