☰
设计模式复习指南:从单例到策略,掌握创建型与行为型核心
2026/10/15 6:43:09 网站建设 项目流程

简介:这份设计模式复习资料面向具备一定编程基础、熟悉面向对象开发的研发人员,特别适合工作1至3年的程序员系统梳理与巩固核心概念。文档从设计模式的定义、分类、特点与应用场景切入,逐一讲解单一职责、开放-封闭、依赖倒转、里氏代换、迪米特法则、合成/聚合及接口隔离七大原则,并深入剖析抽象工厂、策略、外观、适配器、组合、单例与中介者七种常用模式,同时对剩余十五种模式进行对比分析。每种模式均配有UML类图、基础代码与实例说明,便于结合实际项目理解模式选型与实现细节。资源为1个docx文档,整体大小约3.66MB,内容详实、结构清晰,涵盖模式对比、选择维度、练习题与模拟试卷,可帮助读者提升代码的可维护性、扩展性和复用性。已有189人学习,适合作为设计模式复习、面试准备或日常开发查阅的实用参考资料。

1. 设计模式复习:为什么经典模式仍然值得你花时间

如果你正在准备软件工程方向的技术面试,或者项目代码越写越乱、每次加需求都要动一堆老代码,那设计模式复习不是纸上谈兵,而是一次必要的技术复盘。很多人对设计模式的印象停留在「背 UML 图、背类名」上,但真正到了实际开发里,会发现模式不是模板套用,而是一组在长期工程中沉淀下来的应对变化、隔离耦合、约束协作的惯用法则——它的价值主要体现在当代码需要扩展时,你不需要到处打补丁,而是能把新行为组装进既有结构中。

这一篇会从三类共九个最常考的经典模式入手:创建型、结构型、行为型,围绕「为了解决什么问题、核心类如何协作、落地时什么参数最容易翻车」来拆解。每个模式都给出可运行的最小代码片段和设计取舍说明,适合正在准备面试的人,也适合做中期重构、觉得模块职责混乱的开发者。你自己上手跑一遍,比盯着概念读十遍更有用。

2. 创建型模式复习:单例、工厂方法、抽象工厂的落地参数与拆解

创建型模式解决的是「怎么把对象创建的过程稳定下来」的问题。它的核心逻辑是:把实例化过程从业务代码中抽离,使得调用方依赖的是稳定的创建入口和抽象类型,而不是某个具体的类构造函数。这三者的常见误用是绕不开的,我们先从最常考的单例开始讲。

2.1 单例模式:懒加载/饿汉式的选型与线程安全参数

单例模式保证某个类全局只有一个实例,全局共享访问入口。面试题目里最常见的不是「会不会写单例」,而是「你的单例会怎么被破坏」。破坏手段包括反射、反序列化和多线程竞争,我们在落地时应考虑这些风险再决定写法。

先看一个实际用得最多的懒加载线程安全写法,使用双重检查锁加 volatile:

public class ConfigManager { private static volatile ConfigManager instance; private final Map<String, String> configStore = new ConcurrentHashMap<>(); private ConfigManager() { loadFromFile(); } public static ConfigManager getInstance() { if (instance == null) { synchronized (ConfigManager.class) { if (instance == null) { instance = new ConfigManager(); } } } return instance; } }

这段代码里 volatile 的作用是防止指令重排导致其他线程拿到一个尚未初始化完的对象。所谓指令重排,是指 JVM 在 new 一个大对象时可能先分配内存、再执行构造函数、最后赋值给变量;如果赋值在构造函数被执行完之前发生了,另一个线程就会访问到一个半成品实例。这是手动实现单例最容易踩的坑,不建议靠 synchronized 整个方法体来图省事,锁的粒度太大了。还需要关注的是反射防护,一般做法是在私有构造函数里加一个判断,已有实例存在时直接抛出异常。

从适用边界看,单例更适合全局配置、连接池、线程池这类天然唯一的资源,而不适合无状态工具类硬套单例,无状态工具类用静态方法更直接。顺带一提,在实际团队协作中单例往往演变成「隐藏的全局状态」,成了测试的绊脚石,所以现在更常见的实践是用依赖注入容器来约束单例生命周期,代码里不出现 getInstance。

2.2 工厂方法模式:把 if-else 分支收编成子类工厂

工厂方法模式意图是定义一个创建对象的接口,让子类决定实例化哪一个类。它解决的是「多个相似类型之间的切换逻辑散落在业务代码里」的问题。看一个订单业务中常见的例子:

// 订单解析器接口 public interface OrderParser { Order parse(String rawData); } public class JsonOrderParser implements OrderParser { public Order parse(String rawData) { /* JSON 解析逻辑 */ } } public class XmlOrderParser implements OrderParser { public Order parse(String rawData) { /* XML 解析逻辑 */ } } // 工厂:把格式判断收敛到一个方法里 public class OrderParserFactory { public OrderParser createParser(String format) { if ("json".equalsIgnoreCase(format)) { return new JsonOrderParser(); } else if ("xml".equalsIgnoreCase(format)) { return new XmlOrderParser(); } throw new IllegalArgumentException("不支持的订单格式: " + format); } }

我把这个例子和策略模式摆在一起对比过很多次:它们在结构上确实很像,但意图不同。工厂方法关注的是「创建哪个对象」,策略模式关注的是「替换哪个行为」。同一个 switch 分支结构,如果每次分支内部都 new 出一个对象,属于工厂;如果分支内部是调用不同算法,属于策略。初学者容易在这两者上混淆,面试时讲不清意图是会被追问的。另外,工厂方法适用场景里通常还伴随一句「创建逻辑相对简单」,如果每个分支创建的类初始化参数差异极大,建议直接写成不同工厂类,而不是把分支无限堆长。

2.3 抽象工厂模式:产品族一致性与组合爆炸的取舍

抽象工厂模式是工厂方法的升级版,它解决的问题是「多个配套产品需要保持一致地创建」。最经典的例子是跨平台 UI 组件渲染,一个系统里所有按钮、输入框、对话框必须风格统一。简单来说:抽象工厂管了一条产品线,细节是这一族产品内部有调用关联。

public interface ThemeFactory { Button createButton(); Dialog createDialog(); } public class DarkThemeFactory implements ThemeFactory { public Button createButton() { return new DarkButton(); } public Dialog createDialog() { return new DarkDialog(); } } public class LightThemeFactory implements ThemeFactory { public Button createButton() { return new LightButton(); } public Dialog createDialog() { return new LightDialog(); } }

抽象工厂的明显缺陷是扩展困难:要新增一种产品成员,比如增加一个 Menu 组件,那么所有具体工厂都得实现 createMenu 方法。所以在实际应用中,我会先确认产品族是否稳定,如果团队的 UI 组件每周都有可能加新成员,那抽象工厂大概率不是首选,组合优于继承或直接面向接口编程会更灵活。在设计评审中我见过多个团队在这种问题上来回折腾:为了「以后支持多主题」硬上了抽象工厂,结果一年过去了主题还是两套,新增组件却成了每次开发的必踩点。抽象工厂是对「稳定的产品族」做约束,不是给不确定性做预留的万能方案。

创建型三个模式放在一起复习时,抓住一条主线:单例管住一个实例,工厂方法管住一个维度(相似类型),抽象工厂管住多个维度(产品族)。创建过程的复杂度和变动频率,决定你选哪一个。

3. 结构型模式复习:适配器、装饰器、代理在真实项目里的组织方式

结构型模式处理的是类与对象之间的关系,目标是把不同抽象层次或不同来源的代码组合成可协作的整体。这类模式的常见特征是「类和对象的层次变多了,但各自职责变得更单一」。

3.1 适配器模式:老接口对接新系统的三个必调位置

适配器模式解决的是「接口不兼容但功能等价」的问题。老系统返回 XML,新系统要 JSON;第三方 SDK 暴露的方法命名与业务习惯差异很大,这些都是适配器的应用场景。

public class LegacyPaymentService { public boolean payByAccount(String accountId, double amount) { // 老系统支付逻辑 return true; } } public interface NewPaymentGateway { PaymentResult pay(PaymentRequest request); } // 适配器:将老接口包装成新接口 public class LegacyPaymentAdapter implements NewPaymentGateway { private final LegacyPaymentService legacyService; public LegacyPaymentAdapter(LegacyPaymentService legacyService) { this.legacyService = legacyService; } public PaymentResult pay(PaymentRequest request) { boolean result = legacyService.payByAccount(request.getAccountId(), request.getAmount()); return PaymentResult.from(result); } }

适配器模式在落地时有一个经常被忽略的边界:它只能做接口转换,不适合在转换过程中夹带大量业务逻辑,比如加积分、发短信。如果适配器越写越厚,说明老系统与现状的差距已经大到靠适配器撑不住的地步,该考虑防腐层或直接重写服务了。一个比较隐蔽的坑是异常处理适配:老系统可能抛出的是 ErrorCode,新接口期望的是异常类型,适配器里要把错误码翻译成业务异常,并且要保留原始错误信息便于排查。从类图上看适配器与门面模式有些相似,但意图完全不同,适配器解决接口不匹配,门面解决的是子系统调用复杂化。

3.2 装饰器模式:动态叠加能力时最容易混淆的两层边界

装饰器模式的核心思想是「用组合代替继承来扩展对象功能」。它解决的问题非常具体:如果一个人物类有技能树、装备、Buff,用继承来实现会形成指数级的类爆炸。装饰器允许你在运行时按需把增强包在原始对象外层。

// 以数据流增强为例 final class FileDataSource implements DataSource { private final String filePath; public FileDataSource(String path) { this.filePath = path; } public void write(byte[] data) { /* 写文件 */ } public byte[] read() { /* 读文件 */ return new byte[0]; } } final class EncryptionDecorator implements DataSource { private final DataSource wrappee; public EncryptionDecorator(DataSource source) { this.wrappee = source; } public void write(byte[] data) { byte[] encrypted = encrypt(data); wrappee.write(encrypted); } public byte[] read() { byte[] raw = wrappee.read(); return decrypt(raw); } }

装饰器模式看起来很简单,但实际使用中边界模糊点很多。最常见的混淆是装饰器和责任链:装饰器要求每一层都完成同一个接口的完整行为,然后在内部调用下一层;责任链则允许在任意一层终止处理或者改变处理路径。另一个坑是「多层装饰后调试定位困难」。线上问题排查时看到一串六层的装饰器嵌套,每一层都做一些数据的转换,出了问题不好定位,所以我通常在项目里约定装饰器层数不超过三层,而且装饰器只做增值处理,不做核心数据处理。

还有一个设计决策值得反复强调:当你说「要给某个类扩展功能」的时候,先想清楚是不是可以通过改原类解决,如果不是,再看装饰器是否是最轻量级的方案。为了加一个日志输出而新建一个 LoggingDecorator,通常不如直接改原类或使用 AOP 切面来得直接,装饰器更适用场景是增强组合具备高度不确定性,且你无法控制原类型的源码。

3.3 代理模式:静态代理与动态代理的控制点差异

代理模式与装饰器结构非常相似,关键差别在于意图——代理控制的是对原对象的「访问权限和生命周期」,装饰器增强的是「功能」。「代理」和「装饰器」的类图几乎一样,面试里很爱考这个区分,很多人在上机题里就是因为意图讲不清丢的分。

代理模式的典型应用是延迟加载。一个重量级对象初始化非常耗时,但业务上不是每次调用都真正需要它,那就用代理先占位。

public interface ReportGenerator { String generate(); } public class HeavyReportGenerator implements ReportGenerator { public HeavyReportGenerator() { /* 模拟耗时初始化,比如建立数据库连接 */ } public String generate() { return "heavy report"; } } public class LazyReportProxy implements ReportGenerator { private HeavyReportGenerator realGenerator; public String generate() { if (realGenerator == null) { realGenerator = new HeavyReportGenerator(); } return realGenerator.generate(); } }

动态代理在这里更工程化。Java 的 Proxy 和字节码增强技术可以拦截所有方法调用,统一做权限校验、日志记录,业务代码里基本无感知。比如某个服务的每个方法都要校验角色权限,手写代理类太重复,动态代理可以在不改变原类的前提下统一处理。注意动态代理是有性能损耗的,调用频繁的小方法被代理后会明显变慢,所以线上某些极端高并发路径会把动态代理换成编译期静态代理或字节码直接生成。

在实际项目中,代理模式经常与 Spring AOP 混着提,但这不是同一个概念。AOP 是切面思想,代理是它的实现机制之一;你复习的时候不要把它们强行画等号,要说清「AOP 可以通过动态代理实现,但不是只能用它实现」。

4. 行为型模式复习:策略、观察者、模板方法如何驱动业务逻辑演进

三类模式里行为型最贴近业务代码,因为它管理的不是「创建」也不是「结构」,而是「对象之间的交互方式和职责分配」。「策略」「观察者」「模板方法」也是面试准备中必背的三件套。

4.1 策略模式与 if-else 重构的边界:什么情况下值得做

策略模式的用途一句话就能说明白:定义一族算法,使它们可以互相替换,让算法的变化独立于使用它的客户端。打折活动、运费计算、消息发送渠道这类业务,是最经典的使用场景。

public interface DiscountStrategy { double compute(double price, User user); } public class VipDiscount implements DiscountStrategy { public double compute(double price, User user) { if ("gold".equals(user.getLevel())) return price * 0.8; return price * 0.9; } } public class NoDiscount implements DiscountStrategy { public double compute(double price, User user) { return price; } } // 运行时选择 DiscountStrategy strategy = DiscountStrategyFactory.get(user.getLevel()); double finalPrice = strategy.compute(price, user);

策略模式的核心收益在于「消除分支不是目的,隔离变化才是」。很多开发者看到一个 switch 就急着改成策略模式,这是比较常见的过度设计。分支本身不可怕,只有满足下面两个条件时才值得替换:一是分支内逻辑在持续膨胀,每次加新分支都要动同一个方法,修改频率很高;二是算法确实会在运行时动态切换。如果两者都不满足,一个简单的 switch 可读性更好。

还要注意策略模式会引入大量小类,管理成本是实际存在的。我在某真实项目里见过十几个策略类,每个类就两三行代码,更合适的做法是把策略类改成枚举加函数式接口,大幅减少类文件数量。Java 8 之后的新写法是策略模式的一种演进,如果你在面试里提到「用枚举+Function 替代策略类」,是一个不错的加分点。

4.2 观察者模式:事件通知中的同步阻塞与异常传播盲区

观察者模式定义对象间一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都得到通知。电商下单后的一系列后续动作——扣库存、发短信、送积分、更新推荐——是它最典型的应用。

public class OrderEventBus { private final List<OrderListener> listeners = new CopyOnWriteArrayList<>(); public void register(OrderListener listener) { listeners.add(listener); } public void publish(OrderEvent event) { for (OrderListener listener : listeners) { listener.onOrderCreated(event); } } }

这个简单实现里的第一个坑是同步阻塞:所有监听器按注册顺序依次执行,前面一个监听器慢,后面的全部被拖住。某开发者在项目里就吃过亏:积分服务响应变慢,导致整个下单接口超时。解决方向有几种,最直接的是把监听器丢进线程池执行,但要注意线程池不会继承原有的事务上下文和 traceId,排查问题会变难。第二个坑是异常传播。上面的循环里任何一个 listener 抛出异常,后面的监听器都不会执行了,而且异常会一路传到下单入口,让用户看到一个下单失败但其实订单已经创建成功的奇怪状态。现实项目中一般会约定监听器内部自己捕获异常,或者由事件总线提供隔离执行的能力,保证一个监听器失败不影响其他监听器。

4.3 模板方法模式:固化的流程骨架与变动的步骤节点

模板方法模式定义一个操作中的算法骨架,把一些步骤延迟到子类中实现。它解决的问题是「流程是稳定的,但流程里某些步骤的实现会因为场景不同而变化」——比如数据导入的流程永远是读文件、校验、转换、落库,但每一个具体步骤在不同数据源下有不同实现。

public abstract class DataImporter { // 流程骨架,固定不变 public ImportResult execute(File file) { Data data = readFile(file); validate(data); List<Entity> entities = convert(data); int count = save(entities); return ImportResult.success(count); } protected abstract Data readFile(File file); protected abstract void validate(Data data); protected abstract List<Entity> convert(Data data); protected abstract int save(List<Entity> entities); }

模板方法模式最容易被忽视的点是钩子方法缺失。如果骨架里有些步骤在特定场景下不需要执行,比如「读 Excel」场景不需要校验,正确的做法是在基类里提供一个默认实现或一个可覆盖的钩子,而不是在子类里强行把方法体写空。写空是一种反模式,它相当于用子类行为去破坏父类定义的流程契约。另外一个争议点是模板方法模式和策略模式都强调可替换,但模板方法偏向「同一流程的多版本复用」,策略模式偏向「流程某个环节算法替换」,如果整个流程骨架都会变,说明模板方法不适合当前场景,应该考虑策略模式或从更上层重构。

5. 设计模式的复习路线与排查:怎么从会用走向选对

模式不是学完就结束了。大多数工程师的卡点不在「不会写某个模式」,而在「面对一个具体问题时选错了模式,或选对了却写跑偏」。这一章给出一个实操性较强的复习路线,也把高频崩溃点摊开讲。

5.1 适合软件工程面试与日常重构的两周复习路线

设计模式的知识密度不算低,但不值得拉长战线。通常按两周来排即可,一周过概念和代码,一周做集中应用训练,再留两三天做交叉复盘。

第一周聚焦「每个模式的三要素」:解决的问题、核心参与类、适用边界。以单例为例,你要能说清它解决全局唯一资源访问问题,核心类只有一个 Singleton 自己,边界是依赖注入容器或枚举方式实现时语义会变。第二周要做的是「模式联想训练」:拿到一个场景描述,先别急着写代码,先说出你想到的模式并说明理由。比如「消息推送支持短信、邮件、站内信」——这是策略模式;「每个推送渠道都要记录日志、重试和限流」——这是装饰器模式。

复习顺序上建议先结构型后行为型,结构型枚举明确,好上手;行为型关联业务语义更重,放到后面理解门槛更低。一个普遍的误区是「用背 UML 代替场景理解」——UML 里一个类的职责和历史背景一旦脱离场景,很快就会忘记。

5.2 模式落地排查:选对应付不过去的一刻

把所有模式能出现的问题归一下类,排在最前面的其实是「为了用模式而用模式」。一个单方法不超过 30 行的小模块,硬拆出接口、工厂、策略三件套,后续的维护成本已经超过了它带来的收益。所以在排查任何模式应用时,我的第一问不是「这个类在不在它该在的位置」,而是「这一层抽象是否真的隔离了变化」。如果答案是「没有,任何新需求来了还是得改这里」,那这个模式就是无效的。

第二个常见问题是「把创建型模式做成全局单例滥用」。工厂类本身也是类,如果把工厂也做成单例且内部持有大量线程不安全的成员变量,多线程环境下照样翻车。第三个问题是「区分不清组合和继承的边界」。装饰器和策略模式都在提倡组合优于继承,但具体到场景里,当你在两个相似的类中间复制粘贴方法实现时,就要停下来想想是抽父类还是提取策略对象。

这三个问题每天在真实代码评审里反复出现,属于比较高频的踩坑点。遇到这些问题时先反思模式边界,这比直接重写代码更能提高整体判断力。

6. 设计模式复习的最后一公里:把模式的能量放到复盘与评审中

带着真实项目走一遍设计模式复盘,比刷多少道概念题都更有用。我建议你现在就把最近一个月写过的代码文件列表拉出来,按模式维度给自己打个分。比如你的订单服务里有没有不自觉写出的 if-else 分支长廊?你的接口实现类有没有悄悄超过五个方法、明明可以拆策略却还在往里加?这种复盘会直接告诉你下一步的实践方向。

另外一个值得养成的习惯是在代码评审时问一句:如果用现有的一个经典模式重构这段代码,会更好还是更糟?这一问能把设计模式从「知识储备」变成「日常工具」。某位前辈的做法就很聪明——要求组内新人在每个迭代里至少提交一份「模式重构申请」,写清楚现状代码的问题、要使用的模式、预期的收益和可能引入的复杂度。大部分申请会被驳回,但被驳回过程本身就是一次设计复盘。技术能力就是在这种「反复衡量和取舍」中练出来的,而不是在做完需求后的夸夸其谈里。

最后我分享一个自己踩过的坑。某个功能用了策略模式管理多种通知渠道,代码结构本身没有问题,但我在策略类里直接 new 了依赖对象,没有走依赖注入,结果后续做单元测试时无法 mock 依赖,重构难度极大。从那以后我给自己定了一条规矩:用到模式的同时,必须把对象创建和依赖关系放在容器或工厂层,模式负责行为编排,容器负责生命周期管理,两者分工明确,就不会再互相拖累。

设计模式复习不是「背下来就完事」,它要求你在创建、结构、行为三个维度上都能回答「为什么是它」和「什么时候不是它」。希望这篇能帮你把知识打散重组,在面试和项目里都真正用得上。

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

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

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

立即咨询