概述
设计模式是软件工程的经典智慧结晶,但在实际项目中,工程师常常面临这样的困境:二十三种模式彼此交织,场景相似却语义不同,阅读时了然于心,落笔时举棋不定。《设计模式:可复用面向对象软件的基础》一书为我们提供了概念定义,却没有给出一张从问题域直达具体模式的导航地图。面对“对象创建太复杂该用工厂还是建造者?”“动态增强功能该选代理还是装饰器?”“状态流转和算法替换为何长得一样?”这些问题,即使是资深开发者也可能陷入犹豫。
本文试图构建一套系统化的设计模式决策框架——全局决策树。其核心理念在于:模式选型不应从记忆出发,而应从问题本质出发,通过层层递进的是非问答,逐步收敛到最适配的模式。这套决策树遵循先分大类、再判细节的原则:首先将问题领域归入创建型、结构型、行为型三大分支;然后在各分支内,依据关键特征(如实例数量、接口兼容性、职责分配方式)进行二级分流;最后通过代码特征和设计意图的细微差异做出精准定位。
本文的知识脉络如下:第一部分阐述三大分类的顶层决策逻辑;第二至第四部分分别深入创建型、结构型、行为型的决策子树,每个决策节点均辅以代码片段说明判断依据;第五部分提供二十三模式的综合速查矩阵;第六部分将视野拓展至分布式场景,讨论经典模式在微服务、云原生环境下的形态演化;第七部分通过三个综合实战案例演示从问题到模式的完整推演过程;第八部分精选十五道专家级面试题并给出深度解析。整篇文章可作为设计模式知识体系的终极导航工具,既适合日常编码时的快速检索,也可用于架构设计阶段的系统推敲。
一、设计模式决策树顶层架构
当面对一个具体的设计问题时,第一级分流应判断问题属于哪个宏观范畴。以下流程图展示了从问题描述到三大模式分类的顶层决策路径。
flowchart TD
Start(["设计问题描述"]) --> Q1{"问题核心是关于 对象创建 ?"}
Q1 -->|"是"| C1["创建型模式 Creational Patterns"]
Q1 -->|"否"| Q2{"问题核心是关于 类与对象组合 ?"}
Q2 -->|"是"| C2["结构型模式 Structural Patterns"]
Q2 -->|"否"| C3["行为型模式 Behavioral Patterns"]
C1 --> Detail1["单例/工厂/抽象工厂 建造者/原型"]
C2 --> Detail2["适配器/桥接/组合 装饰器/外观/享元/代理"]
C3 --> Detail3["责任链/命令/解释器 迭代器/中介者/备忘录 观察者/状态/策略 模板方法/访问者"]
style C1 fill:#e1f5fe,stroke:#01579b
style C2 fill:#f3e5f5,stroke:#4a148c
style C3 fill:#e8f5e9,stroke:#1b5e20
图1-1说明:顶层决策树将问题域切分为三个互斥且完备的类别。关键判断在于:如果问题的根源是“如何灵活地创建对象实例”,则进入创建型分支;如果问题是“如何将类或对象组织成更大的结构”,则进入结构型分支;如果问题是“如何分配对象间的职责与协作关系”,则进入行为型分支。实际工程中,一个复杂场景可能同时涉及多个分支的模式,此时应分别对子问题独立应用决策树,最后再评估模式间的组合关系。
三大分类的本质区别与边界模糊情况值得深入辨析:
-
创建型模式关注对象的实例化过程,其核心目标是将对象的创建逻辑与使用逻辑解耦。单例控制实例数量、工厂隐藏构造细节、建造者应对复杂构造、原型利用克隆绕过构造开销。创建型模式处理的都是“new”关键字无法优雅表达的场景。
-
结构型模式关注类或对象的静态组合关系,其核心目标是让原本不兼容的接口协同工作,或者用更灵活的方式组织对象结构。适配器转换接口,装饰器动态附加职责,组合处理树形结构,享元共享内部状态。结构型模式通常在代码编译后结构相对固定,尽管装饰器和代理可以在运行时改变行为,但类之间的关系依然是显式定义的。
-
行为型模式关注对象之间的动态交互与职责分配,其核心目标是描述一组对象如何协作完成单个对象无法完成的任务。观察者建立一对多依赖,策略让算法独立变化,状态使行为随内部状态切换,命令将请求封装为对象。行为型模式通常涉及更复杂的运行时消息传递和状态变迁。
边界模糊情况常见于:桥接模式(结构型)通过组合实现抽象与实现分离,但其中也蕴含了策略选择的意味;访问者模式(行为型)作用于稳定的对象结构,实际上改变了操作与结构的组合方式;装饰器(结构型)与代理(结构型)在代码结构上高度相似,但意图不同——前者强调功能增强,后者强调访问控制。这正是决策树需要深入到细节决策节点的原因。
二、创建型模式决策子树
创建型模式包含五种:单例(Singleton)、工厂方法(Factory Method)、抽象工厂(Abstract Factory)、建造者(Builder)、原型(Prototype)。以下决策树引导您从问题特征出发定位最合适的创建型模式。
flowchart TD
Start(["创建型问题"]) --> Q1{"是否需要严格控制实例数量?"}
Q1 -->|"是"| Q1_1{"是否全局唯一?"}
Q1_1 -->|"是"| Singleton["单例模式 Singleton"]
Q1_1 -->|"否"| Pool["对象池模式* Object Pool"]
Q1 -->|"否"| Q2{"是否希望将对象的创建与使用分离?"}
Q2 -->|"是"| Q2_1{"需要创建的是单个产品等级还是产品族?"}
Q2_1 -->|"单个产品类型"| FactoryMethod["工厂方法模式 Factory Method"]
Q2_1 -->|"相互依赖的产品族"| AbstractFactory["抽象工厂模式 Abstract Factory"]
Q2 -->|"否"| Q3{"对象的构造过程是否非常复杂?"}
Q3 -->|"是"| Builder["建造者模式 Builder"]
Q3 -->|"否"| Q4{"创建新对象的成本是否远高于克隆?"}
Q4 -->|"是"| Prototype["原型模式 Prototype"]
Q4 -->|"否"| DirectNew["直接使用new即可 无需设计模式"]
style Singleton fill:#e1f5fe,stroke:#01579b
style FactoryMethod fill:#e1f5fe,stroke:#01579b
style AbstractFactory fill:#e1f5fe,stroke:#01579b
style Builder fill:#e1f5fe,stroke:#01579b
style Prototype fill:#e1f5fe,stroke:#01579b
图2-1说明:该决策树从创建型问题的四个关键维度入手:实例数量控制、创建与使用分离、构造复杂度、创建成本。对象池模式(Object Pool)虽非GoF 23种模式之一,但在实例数量受限但非唯一(如数据库连接池)时是重要补充,故在图中以灰色标注。如果四个问题的答案均为“否”,则表明场景足够简单,直接使用构造函数即可。
各决策节点的代码判断依据
1. 单例模式判断依据
// 判断依据:全局配置对象、日志组件、线程池等必须唯一
public class AppConfig {
private static volatile AppConfig instance;
private AppConfig() { /* 禁止外部new */ }
public static AppConfig getInstance() {
if (instance == null) {
synchronized (AppConfig.class) {
if (instance == null) {
instance = new AppConfig();
}
}
}
return instance;
}
}
// 误用信号:尝试通过反射或序列化破坏单例
2. 工厂方法模式判断依据
// 判断依据:需要创建不同类型的日志记录器,且由子类决定具体类型
public interface LoggerFactory {
Logger createLogger();
}
public class FileLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
return new FileLogger();
}
}
// 当只有一种产品且创建逻辑简单时,简单工厂可能比工厂方法更合适
3. 抽象工厂模式判断依据
// 判断依据:需要确保UI组件风格一致,如Windows风格和Mac风格互斥但成套
public interface UIFactory {
Button createButton();
Checkbox createCheckbox();
}
public class WindowsUIFactory implements UIFactory {
public Button createButton() { return new WindowsButton(); }
public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}
// 增加新的产品族(如Linux风格)容易,但增加新产品等级(如TextField)困难
4. 建造者模式判断依据
// 判断依据:对象有10+参数,部分可选,且需要分步校验
HttpRequest request = HttpRequest.builder()
.url("https://api.example.com")
.method("POST")
.header("Content-Type", "application/json")
.body(jsonBody)
.timeout(5000)
.retry(3)
.build();
// 建造者模式还可以实现构造过程的分步指导(Director)
5. 原型模式判断依据
// 判断依据:创建对象需要昂贵的数据库查询或网络调用,而克隆成本极低
public class Report implements Cloneable {
private byte[] largeData; // 从数据库加载的庞大数据
@Override
public Report clone() {
try {
return (Report) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
// 注意浅拷贝与深拷贝的区别,原型模式通常需要自定义深拷贝逻辑
创建型模式决策路径解读与边缘案例
在实际项目中,创建型模式选型的难点往往出现在相邻模式的边界区域。
-
工厂方法 vs 抽象工厂:如果系统只需要生产一种产品,但其具体类型需要延迟到子类决定,使用工厂方法;如果需要生产一系列相关产品,并且客户端必须保证这些产品之间能够协同工作,则必须使用抽象工厂。一个边缘案例是:当只有一个产品等级时,抽象工厂退化为工厂方法,此时选择更简单的模式即可。
-
建造者 vs 抽象工厂:两者都创建复杂对象,但侧重点不同。抽象工厂强调创建一组相关的对象,通常是多产品、单次调用返回完整对象;建造者强调分步骤构造单个复杂对象,允许客户端在获取最终对象前进行精细控制。例如,生成不同风格的文档(抽象工厂) vs 逐段构建一份文档(建造者)。边缘案例:若一个产品族的创建也需要分步配置,可以将抽象工厂作为建造者的依赖注入。
-
原型 vs 工厂方法:当产品层次结构非常深,为每一种具体产品都编写对应的工厂类会导致类爆炸时,原型模式可以通过克隆现有实例来规避这一复杂度。Spring框架的Bean作用域中,
prototypescope 正是原型模式的体现。边缘案例:如果克隆过程中需要重置某些状态(如ID、创建时间),原型模式必须提供初始化方法。 -
单例的误用警示:单例模式在Java中是最容易被滥用的模式。当您发现自己在类中定义静态方法仅仅是为了方便调用而不涉及状态时,应当考虑使用静态工具类而非单例。当对象需要被单元测试的Mock框架替换时,单例会引入测试困难,此时应通过依赖注入容器管理单例生命周期。
三、结构型模式决策子树
结构型模式包含七种:适配器(Adapter)、桥接(Bridge)、组合(Composite)、装饰器(Decorator)、外观(Facade)、享元(Flyweight)、代理(Proxy)。决策树围绕接口兼容性、功能增强方式、对象组织结构等维度展开。
flowchart TD
Start(["结构型问题"]) --> Q1{"问题的核心是 接口不兼容吗?"}
Q1 -->|"是"| Q1_1{"不兼容发生在 设计阶段还是 集成阶段?"}
Q1_1 -->|"设计阶段 希望抽象与实现独立变化"| Bridge["桥接模式 Bridge"]
Q1_1 -->|"集成阶段 需要复用已有类"| Q1_2{"需要统一 多个子系统的 复杂接口吗?"}
Q1_2 -->|"是"| Facade["外观模式 Facade"]
Q1_2 -->|"否"| Adapter["适配器模式 Adapter"]
Q1 -->|"否"| Q2{"是否需要为对象 动态增加功能?"}
Q2 -->|"是"| Q2_1{"增强的意图是 控制访问还是 纯粹扩展?"}
Q2_1 -->|"控制访问/延迟加载 日志/权限"| Proxy["代理模式 Proxy"]
Q2_1 -->|"纯粹的功能 扩展/包装"| Decorator["装饰器模式 Decorator"]
Q2 -->|"否"| Q3{"需要处理 整体-部分 树形结构吗?"}
Q3 -->|"是"| Composite["组合模式 Composite"]
Q3 -->|"否"| Q4{"是否大量细粒度对象 导致内存压力?"}
Q4 -->|"是"| Flyweight["享元模式 Flyweight"]
Q4 -->|"否"| DirectComposition["直接组合 或继承即可"]
style Adapter fill:#f3e5f5,stroke:#4a148c
style Bridge fill:#f3e5f5,stroke:#4a148c
style Composite fill:#f3e5f5,stroke:#4a148c
style Decorator fill:#f3e5f5,stroke:#4a148c
style Facade fill:#f3e5f5,stroke:#4a148c
style Flyweight fill:#f3e5f5,stroke:#4a148c
style Proxy fill:#f3e5f5,stroke:#4a148c
图3-1说明:结构型模式的核心区别在于变化的维度。适配器、外观、桥接都解决接口问题,但适配器是事后的补救措施,外观是事前的简化设计,桥接是事中的解耦设计。装饰器与代理在类图上几乎相同,必须通过意图区分:装饰器侧重增强,代理侧重控制。组合模式具有明显的树形递归特征,享元模式则与缓存池概念紧密关联。
各决策节点的代码判断依据
1. 适配器模式判断依据
// 判断依据:第三方日志库接口与现有系统不兼容
interface ModernLogger {
void log(String level, String msg);
}
class LegacyLogger {
void writeLog(String text) { /*...*/ }
}
class LoggerAdapter implements ModernLogger {
private LegacyLogger legacy = new LegacyLogger();
public void log(String level, String msg) {
legacy.writeLog("[" + level + "] " + msg);
}
}
2. 桥接模式判断依据
// 判断依据:多种形状和多种颜色的组合,避免类爆炸
interface Color { void applyColor(); }
class Red implements Color { /*...*/ }
class Blue implements Color { /*...*/ }
abstract class Shape {
protected Color color;
public Shape(Color color) { this.color = color; }
abstract void draw();
}
class Circle extends Shape {
public Circle(Color color) { super(color); }
void draw() { color.applyColor(); System.out.println("圆形"); }
}
// 桥接模式将抽象(Shape)与实现(Color)解耦
3. 组合模式判断依据
// 判断依据:文件系统中的文件和文件夹需要统一操作
interface FileSystemNode {
void ls();
int getSize();
}
class File implements FileSystemNode { /*...*/ }
class Directory implements FileSystemNode {
private List<FileSystemNode> children = new ArrayList<>();
public void ls() {
for (FileSystemNode node : children) {
node.ls();
}
}
}
4. 装饰器模式判断依据
// 判断依据:给输入流动态添加缓冲、解压、加密等功能
InputStream in = new FileInputStream("data.gz");
in = new BufferedInputStream(in);
in = new GZIPInputStream(in);
in = new CipherInputStream(in, cipher);
// 每个装饰器都在不改变接口的前提下增加功能
5. 外观模式判断依据
// 判断依据:一键启动电脑需要调用CPU、内存、硬盘等多个子系统的复杂方法
class ComputerFacade {
private CPU cpu = new CPU();
private Memory memory = new Memory();
private HardDrive hd = new HardDrive();
public void start() {
cpu.freeze();
memory.load(BOOT_ADDRESS, hd.read(BOOT_SECTOR));
cpu.jump(BOOT_ADDRESS);
cpu.execute();
}
}
6. 享元模式判断依据
// 判断依据:文本编辑器中每个字符的字体样式对象数量巨大但种类有限
class GlyphFactory {
private static Map<Character, Glyph> cache = new HashMap<>();
public static Glyph getGlyph(char c) {
return cache.computeIfAbsent(c, k -> new Glyph(k));
}
}
// 内部状态共享,外部状态(位置、颜色等)由客户端维护
7. 代理模式判断依据
// 判断依据:延迟加载大图片,仅在需要显示时才真正加载
class ImageProxy implements Image {
private RealImage realImage;
private String filename;
public void display() {
if (realImage == null) {
realImage = new RealImage(filename); // 真正加载
}
realImage.display();
}
}
结构型模式的边界区分要点
结构型模式之间的混淆是设计模式学习中的经典难题,以下对比揭示关键区分点:
| 模式对 | 相同点 | 不同点 | 判断标准 |
|---|---|---|---|
| 适配器 vs 外观 | 都提供更简单的接口 | 适配器解决接口不匹配,外观简化复杂接口 | 看被包装对象:一个 vs 多个;是否有遗留接口转换需求 |
| 装饰器 vs 代理 | 类图几乎完全一致 | 装饰器由客户端显式组合,代理对客户端透明 | 代理通常控制生命周期(如延迟加载),装饰器不控制 |
| 桥接 vs 策略 | 都使用组合代替继承 | 桥接是结构型,解决抽象与实现分离;策略是行为型,解决算法切换 | 桥接的维度在编译时已确定,策略可在运行时替换 |
| 组合 vs 装饰器 | 都基于递归结构 | 组合旨在统一处理叶子与容器,装饰器旨在动态扩展单一对象 | 组合的树是对象集合,装饰器是单链 |
边缘案例解析:
- 如果需要对一组对象同时进行增强,应该使用组合+装饰器的组合:先用组合将对象组织成树,再用装饰器统一增强每个节点。
- 享元模式与对象池模式极易混淆:享元强调共享内部状态,对象一旦创建,其内部状态不可变;对象池则复用可变对象,每次借出后状态会重置。Java的Integer缓存是享元,数据库连接池是对象池。
- 动态代理(Java Proxy)与装饰器的选择:如果增强逻辑需要应用于一组接口未知的实现,动态代理更合适;如果增强逻辑是显式的、业务相关的,静态装饰器更清晰。
四、行为型模式决策子树
行为型模式数量最多(十一种),覆盖了对象协作的方方面面。以下决策树从职责分配与交互模式的角度进行分流。
flowchart TD
Start(["行为型问题"]) --> Q1{"是否需要在多个对象间传递请求 且每个对象都有机会处理?"}
Q1 -->|"是"| Chain["责任链模式 Chain of Responsibility"]
Q1 -->|"否"| Q2{"是否需要将请求封装为对象 支持撤销/重做/排队?"}
Q2 -->|"是"| Command["命令模式 Command"]
Q2 -->|"否"| Q3{"是否需要解释执行特定领域的语言文法?"}
Q3 -->|"是"| Interpreter["解释器模式 Interpreter"]
Q3 -->|"否"| Q4{"是否需要遍历聚合对象内部元素 并隐藏底层结构?"}
Q4 -->|"是"| Iterator["迭代器模式 Iterator"]
Q4 -->|"否"| Q5{"是否需要解耦多个对象之间的网状通信依赖?"}
Q5 -->|"是"| Mediator["中介者模式 Mediator"]
Q5 -->|"否"| Q6{"是否需要保存对象状态快照以支持恢复?"}
Q6 -->|"是"| Memento["备忘录模式 Memento"]
Q6 -->|"否"| Q7{"对象状态变化时 是否需要自动通知多个依赖对象?"}
Q7 -->|"是"| Observer["观察者模式 Observer"]
Q7 -->|"否"| Q8{"对象的行为是否随内部状态改变而改变?"}
Q8 -->|"是"| State["状态模式 State"]
Q8 -->|"否"| Q9{"是否需要在运行时从一组算法中选择其一?"}
Q9 -->|"是"| Strategy["策略模式 Strategy"]
Q9 -->|"否"| Q10{"是否定义了一个算法骨架 某些步骤延迟到子类实现?"}
Q10 -->|"是"| Template["模板方法模式 Template Method"]
Q10 -->|"否"| Q11{"是否数据结构稳定 但需要经常新增作用于其上的操作?"}
Q11 -->|"是"| Visitor["访问者模式 Visitor"]
Q11 -->|"否"| Simple["简单的协作方式"]
style Chain fill:#e8f5e9,stroke:#1b5e20
style Command fill:#e8f5e9,stroke:#1b5e20
style Interpreter fill:#e8f5e9,stroke:#1b5e20
style Iterator fill:#e8f5e9,stroke:#1b5e20
style Mediator fill:#e8f5e9,stroke:#1b5e20
style Memento fill:#e8f5e9,stroke:#1b5e20
style Observer fill:#e8f5e9,stroke:#1b5e20
style State fill:#e8f5e9,stroke:#1b5e20
style Strategy fill:#e8f5e9,stroke:#1b5e20
style Template fill:#e8f5e9,stroke:#1b5e20
style Visitor fill:#e8f5e9,stroke:#1b5e20
图4-1说明:行为型决策树的问题链较长,每个问题对应一种核心行为特征。实际判断时,应按顺序排查:如果符合前一模式的典型症状,即可停止。若场景复杂可能同时匹配多个模式(如既需要状态变化通知,又需要封装请求),则考虑模式组合。
各决策节点的代码判断依据
1. 责任链模式
// 判断依据:审批流程,不同金额由不同级别审批
abstract class Approver {
protected Approver next;
public void setNext(Approver next) { this.next = next; }
public abstract void approve(int amount);
}
class Manager extends Approver {
public void approve(int amount) {
if (amount < 1000) { System.out.println("经理审批"); }
else if (next != null) { next.approve(amount); }
}
}
2. 命令模式
// 判断依据:GUI按钮点击、撤销重做
interface Command {
void execute();
void undo();
}
class SaveCommand implements Command {
private Editor editor;
public void execute() { editor.save(); }
public void undo() { editor.revert(); }
}
3. 解释器模式
// 判断依据:SQL解析、正则表达式引擎、规则引擎
interface Expression {
boolean interpret(String context);
}
class OrExpression implements Expression {
private Expression expr1, expr2;
public boolean interpret(String context) {
return expr1.interpret(context) || expr2.interpret(context);
}
}
4. 迭代器模式
// 判断依据:自定义集合遍历,隐藏内部数据结构
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
}
5. 中介者模式
// 判断依据:聊天室中用户不直接通信,而是通过中介者
class ChatRoom {
public static void showMessage(User user, String msg) {
System.out.println(user.getName() + ": " + msg);
}
}
class User {
public void sendMessage(String msg) {
ChatRoom.showMessage(this, msg);
}
}
6. 备忘录模式
// 判断依据:文档的撤销(Ctrl+Z)功能
class EditorMemento {
private final String state;
public EditorMemento(String state) { this.state = state; }
public String getState() { return state; }
}
class Editor {
private String content;
public EditorMemento save() { return new EditorMemento(content); }
public void restore(EditorMemento m) { content = m.getState(); }
}
7. 观察者模式
// 判断依据:股票价格变动通知所有持股人
class Stock extends Observable {
private double price;
public void setPrice(double price) {
this.price = price;
setChanged();
notifyObservers(price);
}
}
8. 状态模式
// 判断依据:订单状态变化导致允许的操作不同
interface OrderState {
void pay(Order order);
void ship(Order order);
}
class PendingState implements OrderState {
public void pay(Order order) {
order.setState(new PaidState());
}
public void ship(Order order) { throw new IllegalStateException(); }
}
9. 策略模式
// 判断依据:支付方式选择(微信/支付宝/银行卡)
interface PaymentStrategy {
void pay(double amount);
}
class WechatPay implements PaymentStrategy { /*...*/ }
class ShoppingCart {
public void checkout(PaymentStrategy strategy) {
strategy.pay(total);
}
}
10. 模板方法模式
// 判断依据:JUnit测试生命周期
abstract class TestCase {
public final void run() {
setUp();
doTest();
tearDown();
}
protected abstract void doTest();
}
11. 访问者模式
// 判断依据:编译器对AST进行语法检查、类型检查、代码生成等不同操作
interface Visitor {
void visit(AssignmentNode node);
void visit(VariableNode node);
}
class TypeCheckVisitor implements Visitor { /*...*/ }
易混淆行为型模式深度辨析
行为型模式中,以下四对组合在面试和实际设计中极易混淆,需要从意图、状态依赖、生命周期三个维度加以区分。
1. 策略模式 vs 状态模式
| 对比维度 | 策略模式 | 状态模式 |
|---|---|---|
| 意图 | 算法可互换,客户端主动选择 | 对象行为随内部状态变化而自动改变 |
| 状态感知 | 策略类通常无状态,不感知上下文 | 状态类持有上下文引用,能主动触发转换 |
| 转换触发 | 由客户端在外部决定切换时机 | 由状态类自身决定何时切换到下一个状态 |
| 典型场景 | 电商支付方式、压缩算法选择 | TCP连接状态、订单生命周期、游戏角色状态 |
判断方法:问自己一个问题——“对象是否知道自己处在什么状态?”如果答案是“是”,并且不同状态下可执行的操作集合不同,则为状态模式;如果仅仅是一组可替换的算法,对象本身不关心用哪个算法,则为策略模式。
2. 观察者模式 vs 中介者模式
两者都涉及多个对象间的通信解耦,但解耦的层次不同:
- 观察者模式是一对多的松耦合,主题只知道观察者接口,观察者需主动订阅。
- 中介者模式是多对多的星型结构,所有通信都经过中介者,同事类之间完全不感知。
判断标准:如果对象之间的交互是单向通知(如事件广播),用观察者;如果交互是双向协作且逻辑复杂(如UI组件联动),用中介者。
3. 命令模式 vs 策略模式
两者都封装行为,但封装粒度和目的不同:
- 命令模式封装的是请求,强调请求的发送者与接收者解耦,并支持撤销/重做、排队、日志等操作。
- 策略模式封装的是算法,强调算法的可替换性,客户端知道具体的策略类型。
代码层面的区分:命令模式通常包含execute()和undo()方法,而策略模式只有算法执行方法。命令模式的接收者是外部传入或构造函数绑定的,策略模式的算法逻辑内聚在策略类自身。
4. 责任链模式 vs 装饰器模式
两者都形成链式结构,但链的本质不同:
- 责任链中的每个处理器可能终止请求的传递,处理器之间是互斥的“竞标”关系。
- 装饰器链中的每个装饰器都会执行,且执行顺序影响最终结果。
判断方法:如果在链的某一环处理完成后,后续环节无需再处理,则是责任链;如果每个环节都必须执行(如加密→压缩→编码),则是装饰器。
五、综合决策矩阵速查表
下表汇总了二十三种模式的一句话意图、适用场景、框架案例及误用信号,可作为快速检索的索引。
| 模式名称 | 一句话意图 | 一句话适用场景 | 典型框架案例 | 误用信号(何时不该用) |
|---|---|---|---|---|
| 单例 | 确保类只有一个实例并提供全局访问点 | 全局配置、日志器、线程池 | Spring Bean(默认单例) | 仅为了“方便调用”而使用,破坏可测试性 |
| 工厂方法 | 定义创建对象的接口,由子类决定实例化哪个类 | 产品类型单一,但具体类需延迟确定 | Spring的FactoryBean | 产品种类过多导致工厂类爆炸 |
| 抽象工厂 | 创建一系列相关或依赖的对象,无需指定具体类 | UI主题切换、跨平台组件库 | JDBC Connection创建Statement | 增加新产品等级困难,违反开闭原则 |
| 建造者 | 分步构造复杂对象,使构造过程可精细控制 | 参数多、需校验、不可变对象构建 | Lombok @Builder,OkHttpClient | 参数少于4个时显得过度设计 |
| 原型 | 通过克隆现有实例创建新对象 | 对象创建成本高,且运行时需动态复制 | Spring Bean作用域Prototype | 克隆时未处理好深拷贝导致状态污染 |
| 适配器 | 转换接口,使不兼容的类能够协作 | 集成遗留系统、对接第三方SDK | Arrays.asList,InputStreamReader | 接口差异可通过重构消除时不应用 |
| 桥接 | 将抽象与实现解耦,使两者可独立变化 | 多种品牌和多种类型的组合 | JDBC驱动(Driver与Connection) | 只有一个变化维度时,简单继承即可 |
| 组合 | 将对象组织成树形结构,统一处理叶子与容器 | 文件系统、UI组件树、组织架构 | JSF组件树,AWT容器 | 叶子与容器行为差异过大时强制统一 |
| 装饰器 | 动态给对象添加额外职责,比继承更灵活 | I/O流、数据流处理管道 | java.io包各种Stream | 装饰逻辑影响对象核心行为语义时 |
| 外观 | 为子系统中的一组接口提供统一高层接口 | 简化复杂子系统的调用 | SLF4J门面,Spring JdbcTemplate | 子系统本身足够简单时无需包装 |
| 享元 | 共享细粒度对象以高效支持大量相似对象 | 文本编辑器字符格式、游戏粒子系统 | Integer.valueOf(-128~127缓存) | 外部状态维护复杂,导致逻辑分散 |
| 代理 | 为其他对象提供替身以控制访问 | 延迟加载、权限控制、远程调用 | Spring AOP,Hibernate懒加载 | 代理逻辑复杂时增加调试难度 |
| 责任链 | 允许多个对象都有机会处理请求,避免耦合 | 审批流、Servlet过滤器链 | Netty Pipeline,Spring Security Filter | 链过长影响性能且不易调试 |
| 命令 | 将请求封装为对象,支持参数化与撤销 | GUI按钮、宏命令、任务队列 | Runnable接口,Job | 命令本身逻辑简单无需额外封装 |
| 解释器 | 定义文法表示并解释执行特定语言 | 规则引擎、SQL解析、正则表达式 | Spring EL表达式,Drools规则引擎 | 文法过于复杂导致类爆炸 |
| 迭代器 | 顺序访问聚合对象元素,不暴露内部表示 | 遍历集合、文件行读取 | Java Iterator,Stream | 直接用for-each更简洁时不必自定义 |
| 中介者 | 定义中介对象封装一组对象的交互 | GUI组件联动、聊天室、航空管制 | Spring MVC DispatcherServlet | 中介者自身变成“上帝类” |
| 备忘录 | 捕获对象内部状态并在外部保存,支持恢复 | 编辑器撤销、事务回滚 | Serializable深拷贝 | 状态数据量大时消耗内存 |
| 观察者 | 定义一对多依赖,状态变化时自动通知 | 事件驱动、消息订阅、数据绑定 | Java EventListener,RxJava | 同步通知阻塞主流程时考虑异步 |
| 状态 | 对象行为随内部状态改变而改变 | 订单状态机、TCP连接、游戏状态 | Spring StateMachine | 状态数量少且转换逻辑简单时用if-else即可 |
| 策略 | 定义算法族,使算法可独立替换 | 支付方式、排序算法、压缩算法 | Comparator接口,Spring Resource | 算法数量少且稳定时直接硬编码 |
| 模板方法 | 定义算法骨架,延迟步骤到子类 | 框架生命周期、测试用例 | JUnit TestCase,Spring JdbcTemplate | 步骤变化过多导致子类膨胀 |
| 访问者 | 数据结构稳定时,定义作用于元素的新操作 | 编译器AST操作、报表生成 | ASM字节码操作,JSON遍历 | 元素类频繁变化导致Visitor接口不断修改 |
速查指南
使用本矩阵表时,请遵循以下三步法:
- 确定大类:根据问题域判断是创建型、结构型还是行为型,缩小候选范围至5~11种模式。
- 扫描意图列:快速阅读候选模式的“一句话意图”,圈定2~3个最匹配的选项。
- 核对误用信号:对照“误用信号”列,排除掉在当前场景下可能带来副作用的模式,最终确定最优解。
例如,面对“订单支付成功后需通知库存系统和物流系统”的场景:首先确定为行为型(对象交互);扫描意图发现“观察者”符合一对多通知;核对误用信号——若通知逻辑简单且同步执行,观察者模式完全适用。若通知链路可能阻塞主流程,则需结合消息队列异步化。
六、分布式场景下的模式选型扩展
经典GoF设计模式诞生于单机时代,但在当今微服务、云原生架构下,这些模式的思想依然深刻影响着分布式系统设计。然而,实现机制从内存方法调用转变为网络通信与共享存储,选型时需额外考虑CAP约束、网络延迟、部分失败等分布式特性。
以下决策树展示了在分布式环境下,经典模式对应的扩展形态。
flowchart TD
Start(["分布式设计问题"]) --> Q1{"是否需要全局唯一实例或单点协调?"}
Q1 -->|"是"| DS1["分布式单例 基于Redis锁/ZK选举"]
Q1 -->|"否"| Q2{"是否需要一对多异步状态通知?"}
Q2 -->|"是"| DS2["发布-订阅 消息队列"]
Q2 -->|"否"| Q3{"是否需要封装远程请求为可调度任务?"}
Q3 -->|"是"| DS3["分布式命令 任务调度框架"]
Q3 -->|"否"| Q4{"是否需要为远程服务提供本地代表?"}
Q4 -->|"是"| DS4["远程代理 RPC Stub/Skeleton"]
Q4 -->|"否"| Q5{"是否需要统一多个微服务的复杂入口?"}
Q5 -->|"是"| DS5["API网关 外观模式"]
Q5 -->|"否"| Q6{"是否需要共享大量热点数据以减少DB压力?"}
Q6 -->|"是"| DS6["分布式缓存池 享元模式"]
Q6 -->|"否"| Q7{"请求是否需要经过一系列前置处理?"}
Q7 -->|"是"| DS7["网关过滤器链 责任链模式"]
style DS1 fill:#fff3e0,stroke:#e65100
style DS2 fill:#fff3e0,stroke:#e65100
style DS3 fill:#fff3e0,stroke:#e65100
style DS4 fill:#fff3e0,stroke:#e65100
style DS5 fill:#fff3e0,stroke:#e65100
style DS6 fill:#fff3e0,stroke:#e65100
style DS7 fill:#fff3e0,stroke:#e65100
图6-1说明:分布式模式扩展的核心是将本地协作转化为远程协作。每个扩展形态都继承了经典模式的设计意图,但实现技术和权衡点发生了根本变化。例如,单例模式在单机下通过JVM类加载器保证,在分布式下则需借助Redis的SETNX或ZooKeeper的临时顺序节点实现Leader选举,且需考虑脑裂问题。
经典模式在分布式环境下的映射与影响
| 经典模式 | 分布式扩展形态 | 关键技术 | 新增考量维度 |
|---|---|---|---|
| 单例 | 分布式锁/Leader选举 | Redis Redlock、ZK Curator | 网络分区、脑裂、租约续期 |
| 观察者 | 发布-订阅 | Kafka、RabbitMQ、Redis Pub/Sub | 消息可靠投递、消费幂等、顺序性 |
| 命令 | 分布式任务调度 | Quartz集群、XXL-JOB、SchedulerX | 任务分片、故障转移、执行日志 |
| 代理 | RPC远程代理 | gRPC Stub、Dubbo Proxy | 序列化协议、超时重试、熔断降级 |
| 外观 | API网关 | Spring Cloud Gateway、Kong | 路由匹配、限流、鉴权、协议转换 |
| 享元 | 分布式缓存池 | Redis、Memcached、Caffeine | 缓存一致性、穿透/击穿/雪崩、淘汰策略 |
| 责任链 | 微服务网关过滤器链 | GatewayFilter、Netty ChannelHandler | 异步非阻塞、异常熔断、顺序保证 |
分布式环境的特殊影响分析:
-
单例模式的失效与重生:在JVM内,
private static volatile保证了单例的绝对唯一。但在分布式集群中,每个JVM实例都有自己的单例副本。若业务需要全局唯一执行者(如唯一的任务调度器、唯一的ID生成器),则必须升级为分布式单例。实现时需注意:Redis锁要考虑锁续期(Redisson看门狗机制),ZK选举要注意Session超时与重连策略。 -
观察者模式的异步必然性:单机下观察者的
notifyObservers()通常同步执行,但在分布式系统中,直接跨网络同步调用观察者会引入严重的延迟和可靠性问题。因此,消息队列的发布-订阅成为事实标准。设计时需明确:消息投递语义(at-most-once / at-least-once / exactly-once)、消费失败的重试策略、死信队列处理。 -
命令模式的任务持久化:单机命令模式可将命令对象序列化到本地文件实现撤销。分布式命令(任务)则需要将任务信息持久化到数据库,并由调度中心分配执行。此时命令对象必须设计为无状态的、可序列化的,执行结果需支持幂等回调。
-
代理模式的透明性挑战:单机代理(如Spring AOP)通过字节码增强实现完全透明。RPC远程代理虽然也实现了相同接口,但网络异常、超时等分布式问题无法完全透明化。设计时需明确接口的失败语义,合理使用熔断、降级、重试等容错手段。
-
外观模式的网关扩展:API网关不仅统一了微服务的入口,更承担了跨横切关注点(Cross-cutting Concerns)的职责,包括认证、限流、日志、协议转换等。这是外观模式在分布式下的高级形态,但需注意网关不应承载过重的业务逻辑,否则会成为单点瓶颈。
-
享元模式的数据一致性权衡:单机享元(如Integer缓存)内部状态不变,无需考虑一致性。分布式缓存(如Redis)中的数据可能被多节点并发修改,必须根据业务对一致性的敏感度选择合适策略(Cache-Aside、Write-Through、Write-Behind)。
七、实战演练:从问题到模式的完整推演
以下通过三个真实场景,完整演示如何运用决策树从问题描述推导出最优模式组合。
案例一:电商订单状态流转与多渠道通知
1. 问题场景描述
我们需要实现一个电商订单系统,订单存在待支付、已支付、已发货、已完成、已取消五种状态。不同状态下允许的操作不同(如待支付可取消、可支付;已发货可确认收货)。当订单状态变更时,需要通知用户(短信、App推送、邮件),并同步更新积分系统、库存系统等下游模块。
2. 按决策树逐步推演
- 顶层分类:问题核心是“对象交互与职责分配”(状态变化触发多方行为),属于行为型。
- 行为型决策树排查:
- Q8:“对象的行为是否随内部状态改变而改变?” → 是。订单在不同状态下允许的操作集合不同,符合状态模式。
- Q7:“对象状态变化时,是否需要自动通知多个依赖对象?” → 是。状态变更需通知用户和下游系统,符合观察者模式。
- 是否还有其他模式参与:通知渠道(短信/推送/邮件)的选择是典型的策略模式;为防止订单构造参数过多,可使用建造者模式创建订单对象。
3. 最终选定的模式组合及理由
| 模式 | 在场景中的作用 | 选型理由 |
|---|---|---|
| 状态模式 | 封装订单在各个状态下的行为,消除庞大的if-else | 状态数量多(5种),且未来可能扩展(如增加“退款中”),状态模式符合开闭原则 |
| 观察者模式 | 订单状态变更时,通知所有感兴趣的监听器 | 状态变更的后续动作多样且易变,观察者模式解耦了订单对象与通知逻辑 |
| 策略模式 | 根据用户偏好选择合适的通知渠道 | 通知方式可互相替换,且由外部配置决定,符合策略模式意图 |
| 建造者模式 | 构造包含10+字段的订单对象 | 订单包含收货地址、商品列表、优惠信息等复杂参数,建造者提供清晰构造方式 |
4. 简化代码骨架与Mermaid类图
// 状态模式核心接口
interface OrderState {
void pay(OrderContext context);
void cancel(OrderContext context);
void ship(OrderContext context);
void confirm(OrderContext context);
}
class PendingPaymentState implements OrderState {
public void pay(OrderContext ctx) {
// 支付逻辑
ctx.setState(new PaidState());
ctx.notifyObservers(OrderEvent.PAID);
}
public void cancel(OrderContext ctx) {
ctx.setState(new CancelledState());
ctx.notifyObservers(OrderEvent.CANCELLED);
}
// 其他方法抛异常
}
// 观察者模式
interface OrderObserver {
void update(OrderEvent event, OrderContext ctx);
}
class SmsNotificationObserver implements OrderObserver { /*...*/ }
class InventoryUpdateObserver implements OrderObserver { /*...*/ }
// 订单上下文(同时充当Subject和StateContext)
class OrderContext extends Observable {
private OrderState state;
private List<OrderObserver> observers = new ArrayList<>();
public void pay() { state.pay(this); }
public void addObserver(OrderObserver o) { observers.add(o); }
public void notifyObservers(OrderEvent e) {
observers.forEach(o -> o.update(e, this));
}
}
classDiagram
class OrderContext {
-OrderState state
-List~OrderObserver~ observers
+pay()
+cancel()
+ship()
+confirm()
+setState(OrderState)
+notifyObservers(OrderEvent)
}
class OrderState {
<<interface>>
+pay(OrderContext)
+cancel(OrderContext)
+ship(OrderContext)
+confirm(OrderContext)
}
class PendingPaymentState {
+pay(OrderContext)
+cancel(OrderContext)
}
class PaidState {
+ship(OrderContext)
+cancel(OrderContext)
}
class ShippedState {
+confirm(OrderContext)
}
class OrderObserver {
<<interface>>
+update(OrderEvent, OrderContext)
}
class SmsNotificationObserver {
+update(OrderEvent, OrderContext)
}
class InventoryUpdateObserver {
+update(OrderEvent, OrderContext)
}
OrderContext --> OrderState
PendingPaymentState ..|> OrderState
PaidState ..|> OrderState
ShippedState ..|> OrderState
OrderContext --> OrderObserver
SmsNotificationObserver ..|> OrderObserver
InventoryUpdateObserver ..|> OrderObserver
图7-1说明:类图展示了状态模式与观察者模式的结合。OrderContext既是状态的持有者(上下文),也是被观察的目标(Subject)。当客户端调用pay()等方法时,调用被委托给当前状态对象;状态对象在执行完业务逻辑后,通过setState()切换状态,并调用notifyObservers()触发通知。这种设计使得状态转换规则内聚在各状态类中,通知逻辑则分散在观察者中,两者职责清晰,易于扩展。
案例二:文档导出系统的多格式支持与复杂构建
1. 问题场景描述
需要开发一个报表导出模块,支持将同一份业务数据导出为PDF、Word、Excel三种格式。每种格式的导出过程都包含:①加载模板、②填充数据、③格式校验、④添加水印、⑤生成文件流等步骤。部分格式可能还需额外步骤(如Excel需合并单元格)。用户可以根据需要选择导出格式,并配置水印文字、纸张大小等选项。
2. 按决策树逐步推演
- 顶层分类:问题涉及“如何灵活创建复杂的导出对象”,属于创建型。
- 创建型决策树排查:
- Q1:“需要控制实例数量吗?” → 否。
- Q2:“需要将对象创建与使用分离吗?” → 是。客户端不关心具体导出器的创建细节,且导出器类型由运行时参数决定。需要创建的是单个产品类型(Exporter),但有不同的变体(PDF/Word/Excel),符合工厂方法模式。
- Q3:“对象的构造过程是否非常复杂?” → 是。导出过程需分步执行(加载模板、填充数据等),且步骤顺序可能变化,符合建造者模式。
- 跨分支检查:导出步骤(算法骨架)固定,但具体步骤实现在各格式中不同,这是典型的模板方法模式(行为型)。因此最终是工厂方法 + 建造者 + 模板方法的组合。
3. 最终选定的模式组合及理由
| 模式 | 在场景中的作用 | 选型理由 |
|---|---|---|
| 工厂方法 | 根据用户选择的格式创建对应的Exporter子类 | 将导出器创建与使用分离,新增格式只需添加新的工厂子类 |
| 建造者模式 | 分步骤配置导出选项(水印、纸张等),并控制导出流程 | 导出参数多且可选,建造者提供流式API,比构造函数更清晰 |
| 模板方法模式 | 定义导出算法的骨架,各步骤延迟到子类实现 | 导出流程固定(模板→数据→校验→水印→输出),各格式仅需实现差异部分 |
4. 简化代码骨架与Mermaid类图
// 模板方法模式:抽象导出器
abstract class Exporter {
public final void export(ExportContext ctx) {
loadTemplate();
fillData(ctx);
validate();
addWatermark(ctx.getWatermark());
writeOutput();
}
protected abstract void loadTemplate();
protected abstract void fillData(ExportContext ctx);
protected void validate() { /* 默认实现 */ }
protected void addWatermark(String text) { /* 钩子方法 */ }
protected abstract void writeOutput();
}
class PDFExporter extends Exporter {
protected void loadTemplate() { /* PDF模板加载 */ }
// 实现其他抽象方法...
}
// 工厂方法模式:创建具体的Exporter
abstract class ExporterFactory {
public abstract Exporter createExporter();
}
class PDFExporterFactory extends ExporterFactory {
public Exporter createExporter() { return new PDFExporter(); }
}
// 建造者模式:配置导出选项
class ExportContextBuilder {
private String watermark;
private PageSize pageSize;
// ... 其他配置
public ExportContextBuilder watermark(String w) { this.watermark = w; return this; }
public ExportContext build() { return new ExportContext(this); }
}
classDiagram
class Exporter {
<<abstract>>
+export(ExportContext) void
#loadTemplate() void*
#fillData(ExportContext) void*
#validate() void
#addWatermark(String) void
#writeOutput() void*
}
class PDFExporter {
+loadTemplate() void
+fillData(ExportContext) void
+writeOutput() void
}
class WordExporter {
+loadTemplate() void
+fillData(ExportContext) void
+writeOutput() void
}
class ExporterFactory {
<<abstract>>
+createExporter() Exporter*
}
class PDFExporterFactory {
+createExporter() Exporter
}
class ExportContextBuilder {
-watermark : String
+watermark(String) ExportContextBuilder
+build() ExportContext
}
Exporter <|-- PDFExporter
Exporter <|-- WordExporter
ExporterFactory <|-- PDFExporterFactory
PDFExporterFactory ..> PDFExporter : creates
Exporter ..> ExportContextBuilder : uses
图7-2说明:该设计将创建、构造、执行三个维度分离。Exporter定义了模板方法export(),保证了导出流程的统一性;子类实现具体步骤,满足了多态性。ExporterFactory将Exporter的实例化延迟到子类,使得客户端可以面向接口编程。ExportContextBuilder则解决了复杂参数对象的构造问题。三者协同,既保持了架构的稳定性,又提供了足够的扩展性。
案例三:API网关的请求过滤与路由转发
1. 问题场景描述
设计一个简易的API网关,接收来自客户端的HTTP请求后,需要依次执行:①IP黑名单检查、②JWT Token验证、③请求日志记录、④限流控制、⑤路由转发到后端微服务。其中部分过滤器(如日志)对所有请求生效,部分(如认证)可配置跳过某些路径。网关还需支持负载均衡策略(轮询/随机/一致性哈希)选择后端实例。
2. 按决策树逐步推演
- 顶层分类:问题涉及“请求经过一系列处理器”,属于行为型。
- 行为型决策树排查:
- Q1:“是否需要在多个对象间传递请求,且每个对象都有机会处理?” → 是。请求依次经过IP检查、认证、日志等过滤器,每个过滤器可决定是否继续传递,这是典型的责任链模式。
- Q9:“是否需要在运行时从一组算法中选择其一?” → 是。负载均衡算法(轮询/随机)可根据配置切换,符合策略模式。
- 结构型模式补位:网关作为所有微服务的统一入口,隐藏了后端服务的复杂拓扑,这是外观模式的分布式体现。
3. 最终选定的模式组合及理由
| 模式 | 在场景中的作用 | 选型理由 |
|---|---|---|
| 责任链模式 | 组织请求处理管道,各过滤器独立且可插拔 | 过滤器数量可能增长,责任链提供了灵活的添加/移除机制 |
| 策略模式 | 封装负载均衡算法,支持运行时切换 | 算法可互相替换,且未来可能增加新算法(如加权轮询) |
| 外观模式 | 为后端服务集群提供统一入口 | 简化客户端调用,隐藏内部拓扑变化 |
4. 简化代码骨架与Mermaid类图
// 责任链模式:过滤器接口
interface GatewayFilter {
void filter(Request req, Response res, FilterChain chain);
}
class IpBlacklistFilter implements GatewayFilter {
public void filter(Request req, Response res, FilterChain chain) {
if (isBlacklisted(req.getIp())) {
res.setStatus(403);
return;
}
chain.doFilter(req, res);
}
}
class FilterChain {
private List<GatewayFilter> filters;
private int index = 0;
public void doFilter(Request req, Response res) {
if (index < filters.size()) {
GatewayFilter filter = filters.get(index++);
filter.filter(req, res, this);
} else {
route(req, res); // 责任链末端,执行路由转发
}
}
}
// 策略模式:负载均衡算法
interface LoadBalancer {
Server select(List<Server> servers);
}
class RoundRobinBalancer implements LoadBalancer { /*...*/ }
class RandomBalancer implements LoadBalancer { /*...*/ }
// 外观模式:Gateway统一入口
class ApiGateway {
private FilterChain chain;
private LoadBalancer balancer;
public Response handle(Request req) {
Response res = new Response();
chain.doFilter(req, res);
return res;
}
}
classDiagram
class GatewayFilter {
<<interface>>
+filter(Request, Response, FilterChain)
}
class IpBlacklistFilter {
+filter(Request, Response, FilterChain)
}
class JwtAuthFilter {
+filter(Request, Response, FilterChain)
}
class RateLimiterFilter {
+filter(Request, Response, FilterChain)
}
class FilterChain {
-List~GatewayFilter~ filters
-int index
+doFilter(Request, Response)
+addFilter(GatewayFilter)
}
class LoadBalancer {
<<interface>>
+select(List~Server~) Server
}
class RoundRobinBalancer {
+select(List~Server~) Server
}
class RandomBalancer {
+select(List~Server~) Server
}
class ApiGateway {
-FilterChain chain
-LoadBalancer balancer
+handle(Request) Response
}
IpBlacklistFilter ..|> GatewayFilter
JwtAuthFilter ..|> GatewayFilter
RateLimiterFilter ..|> GatewayFilter
FilterChain o--> GatewayFilter : contains
ApiGateway --> FilterChain
ApiGateway --> LoadBalancer
RoundRobinBalancer ..|> LoadBalancer
RandomBalancer ..|> LoadBalancer
图7-3说明:ApiGateway作为外观,对外暴露简单的handle接口。内部FilterChain管理一系列GatewayFilter实现责任链模式。过滤器可自由组合,且符合开闭原则(新增过滤器无需修改现有代码)。LoadBalancer策略接口使得路由算法可独立变化。整个设计清晰体现了三种模式的协作:外观简化入口,责任链处理横切关注点,策略提供算法灵活性。
八、面试题精选与专家级解答
以下面试题旨在考察候选人对设计模式选型判断力,而非死记硬背的定义。
1. 如何快速区分策略模式和状态模式?给出一个实际场景判断方法。
答:核心判断标准是状态感知。策略模式中,上下文类(Context)只是持有一个策略对象的引用,策略对象不知道上下文的存在,切换由客户端主动发起。状态模式中,状态对象持有上下文的引用,并且能够根据业务逻辑主动触发状态转换。
场景判断法:观察对象是否能够“自我演进”。例如订单对象,当前状态为“待支付”,执行支付操作后,订单自己变成了“已支付”状态,这是状态模式。而支付方式选择(微信/支付宝),由用户在支付那一刻从外部指定,且支付方式本身不会改变订单状态,这是策略模式。
2. 装饰器模式和代理模式在代码结构上几乎相同,如何在设计阶段做出正确选择?
答:区分点在意图和生命周期管理。
- 意图:装饰器强调增强功能,客户端可以主动选择如何装饰,典型例子是Java I/O流:
new BufferedInputStream(new FileInputStream(...))。代理强调控制访问,客户端通常无感知,例如Spring AOP代理、Hibernate懒加载代理。 - 生命周期管理:代理通常控制着真实对象的创建和销毁(如虚拟代理延迟加载),而装饰器的真实对象由外部传入,装饰器不管理其生命周期。
设计阶段判断法:问自己一个问题——“如果真实对象已经存在,我们还想增加功能,应该用什么?”答案必然是装饰器。“如果真实对象创建成本很高,或者我们需要在访问前后做权限/日志控制,应该用什么?”答案必然是代理。
3. 什么时候应该选择抽象工厂而不是建造者模式?
答:抽象工厂和建造者都创建复杂对象,但适用场景完全不同。
- 选择抽象工厂:当需要创建一系列相关的产品对象,并且客户端必须以“产品族”为单位使用它们时。例如,一套UI组件库必须保证所有控件风格统一(Windows风格按钮配Windows风格复选框),抽象工厂可以强制这个约束。
- 选择建造者:当需要分步骤构建一个复杂对象,且构建过程需要精细控制时。例如,生成一份报告需要先设置页眉、再填充正文、最后添加页脚,步骤顺序很重要。
关键区分:抽象工厂的create()通常一次性返回完整对象;建造者的build()是在一系列setXxx()之后调用。如果产品之间没有强制的“族”约束,不要使用抽象工厂。
4. 桥接模式和策略模式都涉及组合,它们的选型边界在哪里?
答:两者都使用组合替代继承,但位于不同维度。
- 桥接模式是结构型,关注点在编译时结构。它将抽象部分与实现部分分离,使它们可以独立变化。两个维度在系统设计时就已经确定,且在对象生命周期内通常不变。例如,形状(抽象)和颜色(实现)两个维度。
- 策略模式是行为型,关注点在运行时算法切换。策略对象可以在对象生命周期的任意时刻被替换,以改变对象的行为。例如,电商购物车的支付策略可以在结算前任意切换。
边界区分:如果组合的对象代表一种结构属性,用桥接;如果代表一种可替换的行为,用策略。
5. 外观模式和中介者模式都能简化复杂交互,如何二选一?
答:外观模式是单向简化,中介者模式是多向协调。
- 外观模式:客户端调用外观,外观再调用子系统。子系统从不主动调用外观,也不感知外观的存在。外观只是为复杂的子系统提供一个简单的入口。例如,一键启动电脑的按钮。
- 中介者模式:同事类之间相互通信,但所有通信都通过中介者转发。中介者是同事类之间交互的必经枢纽。例如,聊天室中用户A发消息给用户B,必须通过聊天室服务器。
选择标准:如果子系统之间需要相互通信,用中介者;如果只是客户端觉得子系统太复杂,用外观。
6. 享元模式和单例模式都可以实现对象复用,选型标准是什么?
答:享元复用一类对象,单例复用单个对象。
- 单例模式:确保一个类只有一个实例,适用于全局唯一的组件,如配置管理器、日志记录器。
- 享元模式:通过共享来支持大量细粒度对象。例如,一篇文档中的字符'a'可能有成千上万个,但只需要一个'a'的享元对象来表示其字形信息。
误用警示:如果一个类确实需要多个实例(如不同状态的连接对象),强行使用单例会引入全局状态导致难以测试。如果将享元用于只有少数几个实例的场景,则引入不必要的复杂度。
7. 责任链模式和装饰器模式都有链式结构,如何根据场景区分?
答:链的执行语义不同。
- 责任链:请求在链上传递,每个节点都有权终止传递。一旦某个节点处理了请求,后续节点不再参与。
- 装饰器:请求必须经过链上的每一个节点,每个节点都会执行,并可能对请求或响应进行加工。执行顺序影响最终结果。
场景判断:Servlet Filter是责任链(某个Filter可以return终止),I/O流包装是装饰器(数据必须经过所有Stream)。
8. 在微服务架构中,API网关主要体现了哪些设计模式?
答:API网关是多种模式的集合体:
- 外观模式:为后端众多微服务提供统一的入口,隐藏内部拓扑。
- 责任链模式:网关过滤器链(认证、限流、日志等)是典型责任链。
- 适配器模式:协议转换(如HTTP转gRPC、JSON转XML)。
- 代理模式:网关作为后端服务的远程代理,处理网络细节。
- 策略模式:路由规则、负载均衡策略可动态替换。
9. 如何判断一个场景是否需要使用访问者模式?其代价是什么?
答:判断标准:当一个对象结构(如AST语法树)非常稳定,但经常需要定义新的操作作用于这些元素时,考虑访问者。
适用条件(必须同时满足):
- 被访问的元素类层次结构稳定(很少增加新元素类)。
- 需要频繁增加针对这些元素的新操作。
代价:
- 违反开闭原则于元素方:增加新元素类时,必须修改所有访问者接口和实现。
- 破坏封装:访问者往往需要访问元素的内部状态,导致元素暴露更多细节。
- 双重分派复杂度:实现较复杂,可读性下降。
10. 解释器模式在实际开发中很少用,什么情况下它是必须的选择?
答:当需要定义并执行一门领域特定语言(DSL) 且文法足够简单时。例如:规则引擎、数学公式计算、简单查询语言(如JPA的JPQL、Spring EL)。如果文法复杂(超过20条规则),应使用ANTLR等解析器生成器,而不是手写解释器模式。解释器模式是“最后的手段”,在需要灵活性但不想引入外部解析库时可考虑。
11. 命令模式和策略模式都能封装行为,它们的适用场景有何不同?
答:
- 命令模式:重点在于请求的封装与参数化,支持撤销、排队、日志等。命令接收者通常是在命令外部指定的。典型:Runnable、Job。
- 策略模式:重点在于算法的封装与切换。策略类内部包含了完整的算法逻辑,不依赖于外部接收者。典型:Comparator。
选择标准:如果需要记录操作历史、支持撤销重做,用命令;如果只是想让算法可以插拔,用策略。
12. 组合模式和享元模式能否结合使用?请举例说明选型逻辑。
答:可以结合。例如在文本编辑器中,文档由段落(组合)组成,段落由字符(叶子)组成。每个字符的字形信息(字体、字号)可以使用享元模式共享以节省内存;而文档结构本身使用组合模式统一处理段落和字符的操作(如渲染、拼写检查)。
选型逻辑:先识别出整体-部分树形结构(用组合),再分析叶子节点是否存在大量可共享的不可变内部状态(用享元)。二者解决不同维度的问题,结合时互不干扰。
13. 适配器模式和门面模式都解决接口问题,它们的选型差异是什么?
答:一句话区分:适配器解决“不匹配”,门面解决“太复杂”。
- 适配器:用于事后集成,目的是让一个已存在的类能够适配新接口。典型场景:引入第三方库,其接口与现有系统不兼容。
- 门面:用于事前设计或重构优化,目的是为复杂子系统提供一个简化接口。子系统接口本身是匹配的,只是太多太乱。
14. 在分布式环境下,经典的单例模式失效后,应如何调整选型决策?
答:单例模式在JVM内保证了唯一性,但在分布式集群中,每个节点都有自己的单例,导致全局不再唯一。调整策略如下:
- 若业务要求全局唯一执行者(如全局定时任务唯一执行),需采用分布式锁(Redis)或Leader选举(ZooKeeper/Etcd)实现分布式单例。
- 若业务只是需要一个无状态的服务实例,则单例模式依然适用,但应通过依赖注入容器(如Spring)管理Bean的作用域为Singleton即可,无需额外处理。
15. 当一个场景同时适用多个模式时,如何确定最优选择?
答:遵循奥卡姆剃刀原则和变化预测原则:
- 优先选择最简单的模式:在满足需求的前提下,模式越简单越好。如果能用策略模式解决,不要用状态模式。
- 预测变化方向:思考系统未来最可能在哪方面发生变化(例如,是算法会变,还是状态会变),选择最能应对该变化的模式。
- 考虑代码可读性:模式是为了让人理解设计意图。如果引入模式后代码反而更难理解,则放弃。
- 参考团队技术背景:选择团队最熟悉、维护成本最低的模式。
结语
设计模式是软件工程领域的共同语言,而决策树则是掌握这门语言的语法手册。本文提供的决策框架并非僵化的教条,而是一种思维训练工具。随着使用次数的增加,您将逐渐内化这些判断逻辑,最终达到“无招胜有招”的境界——在阅读需求时,模式的选择会自然浮现。
建议将本文的决策矩阵表打印出来或加入收藏夹,在日常编码遇到设计抉择时快速查阅。也欢迎在实践中验证并补充本文的决策路径,让这套导航工具更加完善。
记住:模式是手段,而非目的。清晰、可维护、可扩展的代码才是终极追求。