前置知识: 软件工程与测试

设计模式详解

8 min高级

23种GoF设计模式分类、原理与应用场景。

1. 从”盖房子的预制件”说起

1.1 设计模式是什么

想象盖房子。没有预制件时,每一堵墙、每一扇窗都要现场设计、现场浇筑;有了标准化的预制件(门窗、楼梯、梁柱的标准做法),工程师不用每次”发明”怎么搭,直接套用成熟方案即可。

设计模式(Design Pattern)就是软件设计的”预制件”:它是经过验证的、可复用的解决方案,针对”反复出现的软件设计问题”。

权威定义(GoF《设计模式》):设计模式是对特定场景下反复出现的问题的可复用解决方案的描述。

1.2 三个关键词

关键词含义
特定场景模式不是万能的,只解决特定问题
反复出现是”常见问题”的总结,不是一次性的灵感
可复用可以在不同项目中重复使用

重要认知:设计模式不是”代码模板”(不是抄一段代码),而是”解决问题的思路”——它描述的是”类之间如何组织、对象之间如何协作”,具体实现可以千变万化。

2. 三大分类与设计原则

2.1 三大分类

GoF 把 23 种设计模式分为三类:

类型数量关注点类比
创建型5对象创建机制怎么”造”东西
结构型7对象组合方式怎么”组装”东西
行为型11对象间通信怎么”协作”

2.2 六大设计原则(理解模式的前提)

设计模式是这些原则的具体实现。先理解原则,再看模式就”顺理成章”:

原则说明通俗理解
单一职责(SRP)一个类只有一个变化原因一个人只做一件事
开闭原则(OCP)对扩展开放,对修改关闭加新功能不改旧代码
里氏替换(LSP)子类可以替换父类能替换而不出问题
接口隔离(ISP)接口应该小而专接口不要”大而全”
依赖倒置(DIP)依赖抽象而非具体面向接口编程
迪米特法则最少知识原则不要和陌生人说话

3. 创建型模式:怎么”造”对象

3.1 单例模式(Singleton)

问题:某些对象(配置、连接池、日志)全局只需要一个实例。

解决:确保一个类只有一个实例,并提供一个全局访问点。

// 双重检查锁
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;
    }
}

要点:构造器私有(外部不能 new)、getInstance 提供全局访问、双重检查锁保证并发安全。

注意:单例有争议(全局状态难测试)。现代框架(Spring)默认单例但由容器管理,比手写单例更优雅。

3.2 工厂方法模式(Factory Method)

问题:创建对象时,具体类型可能变化——不想在代码里写死 new ConcreteProduct()。

解决:定义创建对象的接口,让子类决定实例化哪个类。

classDiagram
    class Product
    class ConcreteProduct
    class Creator {
        +FactoryMethod()
    }
    class ConcreteCreator {
        +FactoryMethod()
    }
    Product <|-- ConcreteProduct
    Creator <|-- ConcreteCreator
    ConcreteCreator --> ConcreteProduct

场景:日志系统要支持”文件日志/数据库日志/控制台日志”——用工厂方法,客户端只依赖 Logger 接口,新增日志类型不改客户端。

3.3 抽象工厂模式(Abstract Factory)

问题:需要创建”一组相关对象”,且这一组对象要保持风格一致。

解决:提供一个创建”一组对象”的接口。

flowchart TD
    T0["AbstractFactory"]
    T1["createProductA()"]
    T2["createProductB()"]
    T3["ConcreteFactory1 → ProductA1, ProductB1"]
    T4["ConcreteFactory2 → ProductA2, ProductB2"]
    T0 --> T1
    T0 --> T2
    T2 --> T3
    T3 --> T4

场景:UI 主题系统——“暗色主题”工厂创建暗色按钮+暗色输入框,“亮色主题”工厂创建亮色组件。保证”同一主题的组件风格一致”。

3.4 建造者模式(Builder)

问题:创建”有大量可选参数”的复杂对象时,构造函数参数太多、可读性差。

解决:分步骤构建对象。

User user = User.builder()
    .name("张三")
    .age(25)
    .email("zhangsan@example.com")
    .build();

场景:配置对象、DTO 等”可选字段很多”的对象。Lombok 的 @Builder 就是此模式的实现。

3.5 原型模式(Prototype)

问题:创建”与已有对象相同”的对象时,直接 new 可能很贵(大对象、深拷贝)。

解决:通过克隆已有对象来创建新对象。

public class Prototype implements Cloneable {
    public Prototype clone() {
        return (Prototype) super.clone();
    }
}

场景:游戏中的大量 NPC、文档模板复制。注意深拷贝与浅拷贝的区别。

4. 结构型模式:怎么”组装”对象

4.1 适配器模式(Adapter)

问题:两个类的接口不兼容(“插头不匹配”)。

解决:加一个适配器,把”不匹配的接口”转换为”客户期望的接口”。

Client → [Target接口] ← [Adapter] → [Adaptee]

场景:对接第三方 SDK、旧系统接口——用一个 Adapter 翻译接口,不改业务代码(这就是 DDD 里的防腐层思想,见本模块「DDD」一文)。

4.2 装饰器模式(Decorator)

问题:想动态地给对象添加功能,而不是改类本身。

解决:用”包装”的方式一层层叠加功能。

classDiagram
    class Component
    class ConcreteComponent
    class Decorator
    class ConcreteDecorator
    Component <|-- ConcreteComponent
    Component <|-- Decorator
    Decorator <|-- ConcreteDecorator
InputStream in = new FileInputStream("file.txt");
in = new BufferedInputStream(in);    // 装饰:缓冲
in = new GZIPInputStream(in);         // 装饰:解压

场景:Java IO 流(缓冲、压缩、加密层层包装)、日志(加时间戳、加级别过滤)。装饰器是”叠加功能”最优雅的方式——比继承子类组合更灵活。

4.3 代理模式(Proxy)

问题:想控制对对象的访问(延迟加载、权限控制、远程访问)。

解决:用一个代理对象控制对真实对象的访问。

代理类型说明
虚拟代理延迟创建开销大的对象
远程代理为远程对象提供本地代表
保护代理控制访问权限
智能引用添加额外操作(引用计数)

场景:图片懒加载(虚拟代理)、RPC 客户端(远程代理)、Spring AOP(动态代理)。

4.4 其他结构型模式

模式意图
外观(Facade)为子系统提供统一接口(“门面”)
桥接(Bridge)分离抽象与实现
组合(Composite)树形结构统一处理(文件系统)
享元(Flyweight)共享对象减少内存(字符串池)

5. 行为型模式:对象怎么”协作”

5.1 观察者模式(Observer)

问题:一个对象变化了,多个对象要响应(一对多依赖)。

解决:定义一对多的依赖,主题变化时自动通知所有观察者。

flowchart TD
    S[Subject<br/>attach(observer)<br/>detach(observer)<br/>notify()]
    S -->|Observer.update()| O1[ConcreteObserver1]
    S -->|Observer.update()| O2[ConcreteObserver2]

场景:消息通知、事件监听、发布订阅系统(这就是事件驱动架构的雏形,见本模块「事件驱动架构」一文)。

5.2 策略模式(Strategy)

问题:算法(排序、计费、认证方式)可以替换,且不想用一堆 if/else。

解决:定义算法族,封装后使它们可以互换。

interface SortStrategy {
    void sort(int[] array);
}

class QuickSort implements SortStrategy { ... }
class MergeSort implements SortStrategy { ... }

class Sorter {
    private SortStrategy strategy;
    public void setStrategy(SortStrategy s) { this.strategy = s; }
    public void sort(int[] arr) { strategy.sort(arr); }
}

场景:运费计算(不同地区不同算法)、支付方式(微信/支付宝/银行卡)——用策略替代 if/else 是消除重复 switch 的经典手法(见《代码重构》)。

5.3 模板方法模式(Template Method)

问题:一个算法的”骨架”固定,但其中某些步骤实现不同。

解决:在父类定义算法骨架,子类实现具体步骤。

abstract class DataProcessor {
    public final void process() {  // 模板方法
        readData();
        transformData();
        writeData();
    }
    protected abstract void readData();
    protected abstract void transformData();
    protected void writeData() { /* 默认实现 */ }
}

场景:数据处理流程(读→转换→写)、Spring 的 JdbcTemplate、HttpServlet 的 doGet/doPost。

5.4 其他行为型模式

模式意图
命令(Command)将请求封装为对象(支持撤销/排队)
迭代器(Iterator)顺序访问集合元素
中介者(Mediator)集中管理对象间交互(聊天室)
备忘录(Memento)保存和恢复对象状态(存档)
状态(State)允许对象在状态改变时改变行为
职责链(Chain of Responsibility)沿链传递请求(中间件)
访问者(Visitor)在不改变类的前提下添加操作
解释器(Interpreter)定义语言的文法及解释器

6. 模式选择指南

遇到问题时怎么选模式?按”问题类型”查找:

问题推荐模式
需要唯一实例单例
创建对象逻辑复杂工厂方法/抽象工厂
分步构建复杂对象建造者
接口不兼容适配器
动态添加功能装饰器
一对多依赖观察者
算法可替换策略
算法骨架固定模板方法
请求需要排队/撤销命令
状态驱动的行为状态

7. 常见误区

误区一:设计模式 = 代码模板,背下来就能用。 → 模式是”解决问题的思路”,不是”可以抄的代码”。理解”解决什么问题、为什么这样设计”,比背代码重要得多。

误区二:用得越多越好。 → 恰恰相反!过度使用设计模式是”过度设计”(违背 YAGNI)。能用简单代码解决的,就不要硬套模式。

误区三:23 种模式都要掌握。 → 日常开发 80% 的场景只用 5-6 个模式(单例、工厂、策略、观察者、装饰器、模板方法)。先把常用的用熟,其他的用到再学。

误区四:设计模式是”Java 的事”。 → 模式是语言无关的。JavaScript(回调即策略)、Python(装饰器语法糖)等语言同样适用——只是实现方式不同。

误区五:设计模式能解决所有设计问题。 → 模式解决”局部设计问题”(类与对象组织);系统级的架构问题(微服务、分层)用”架构模式”解决(见本模块「软件架构概述」)。