夜雨聆风学习资料网

ARTICLE · 1267119

23种设计模式-Java源码与使用场景

23种设计模式-Java源码与使用场景

目录

创建型(5 种)

· 01 单例模式(Singleton)

· 02 工厂方法模式(Factory Method)

· 03 抽象工厂模式(Abstract Factory)

· 04 建造者模式(Builder)

· 05 原型模式(Prototype)

结构型(7 种)

· 06 适配器模式(Adapter / Wrapper)

· 07 桥接模式(Bridge)

· 08 组合模式(Composite)

· 09 装饰器模式(Decorator / Wrapper)

· 10 外观模式(Facade)

· 11 享元模式(Flyweight)

· 12 代理模式(Proxy)

行为型(11 种)

· 13 责任链模式(Chain of Responsibility)

· 14 命令模式(Command)

· 15 解释器模式(Interpreter)

· 16 迭代器模式(Iterator)

· 17 中介者模式(Mediator)

· 18 备忘录模式(Memento)

· 19 观察者模式(Observer / Publish-Subscribe)

· 20 状态模式(State)

· 21 策略模式(Strategy)

· 22 模板方法模式(Template Method)

· 23 访问者模式(Visitor)

读之前:什么是设计模式

设计模式不是"语法糖",也不是必须套用的模板。它们是在特定约束下反复出现的、被验证过的对象协作方案,本质是三件事:

· 找变化、封变化:把系统中会变的部分隔离到独立对象里(策略、桥接、状态)。

· 面向接口而非实现:客户端依赖抽象,运行时才决定具体类型(工厂、代理、装饰器)。

· 组合优于继承:用对象组合替代类继承,降低耦合、提升运行时灵活性(装饰器、策略、桥接)。

六大原则(模式背后的共同价值观)

· 单一职责 SRP:一个类只有一个引起它变化的原因;装饰器、迭代器、状态

· 开闭原则 OCP:对扩展开放、对修改关闭;策略、工厂方法、观察者、装饰器

· 里氏替换 LSP:子类必须能替换父类而不破坏程序;所有继承型模式的前提

· 接口隔离 ISP:不要强迫客户端依赖它用不到的接口;适配器、外观

· 依赖倒置 DIP:高层不依赖低层,二者都依赖抽象;抽象工厂、模板方法、依赖注入

· 迪米特法则 LoD:只与直接朋友通信,减少对象间网状耦合;外观、中介者

最重要的一句话:设计模式是解决复杂性的成本与灵活性收益之间的交易。如果一个类只有一种实现、永远不会变,直接 new 就是最好的设计。为了用模式而用模式,产生的复杂度往往超过它解决的问题。

第一篇 创建型(5 种)

这一组解决的是「对象怎么创建」——把创建动作从使用方剥离,隐藏构造细节。

01 单例模式 Singleton

保证一个类在 JVM 中只有一个实例,并提供一个全局访问点。

为什么需要它

有些对象天生就该只有一个:线程池、配置中心、连接池、序列号生成器。如果有两个实例,就会出现资源争抢、状态不一致。单例要解决三件事:① 唯一实例 ② 线程安全 ③ 防破坏(反射、序列化、克隆)。

类结构

图 01 · 单例模式 类结构

Java 实现

// ① 双重检查锁定 DCL:懒加载 + 高性能。volatile 是关键,禁止 2/3 步指令重排

publicfinalclass Singleton implements java.io.Serializable {

privatestaticvolatile Singleton instance;   // volatile:防止其他线程看到"半初始化"对象

private Singleton() {

// 防御反射攻击:构造器被调用说明 instance 已存在

if (instance != null) {

thrownew IllegalStateException("单例对象已存在,禁止反射创建");

        }

    }

publicstatic Singleton getInstance() {

if (instance == null) {                        // 第一次检查:避免每次调用都加锁

synchronized (Singleton.class) {

if (instance == null) {                // 第二次检查:保证只创建一次

                    instance = new Singleton();        // 非原子:分配内存→初始化→赋值引用

                }

            }

        }

return instance;

    }

// 防御反序列化攻击:不重写的话,反序列化会生成一个全新对象

private Object readResolve() { return instance; }

publicvoid doWork() { System.out.println("处理任务..."); }

}

// ② 静态内部类(推荐):JVM 类加载机制天然保证线程安全 + 懒加载

class HolderSingleton {

private HolderSingleton() {}

privatestaticclass Holder {

staticfinal HolderSingleton INSTANCE = new HolderSingleton();  // 首次访问 Holder 才初始化

    }

publicstatic HolderSingleton getInstance() { return Holder.INSTANCE; }

}

// ③ 枚举单例:《Effective Java》作者 Josh Bloch 推荐,一行解决反射与序列化问题

enum EnumSingleton {

    INSTANCE;                                   // 枚举实例由 JVM 保证唯一且天然可序列化

publicvoid doWork() { System.out.println("处理任务..."); }

}

// 使用

EnumSingleton.INSTANCE.doWork();

典型使用场景

· 全局配置对象(ConfigManager)、应用级缓存

· 线程池、数据库连接池、Redis 连接池等资源管理器

· 序列号 / 雪花 ID 生成器(必须唯一否则 ID 冲突)

· 日志对象、注册表、驱动管理器

JDK 与主流框架里的出处

· java.lang.Runtime#getRuntime() —— 标准饿汉单例

· java.awt.Desktop#getDesktop()

· Spring IoC 容器默认作用域 singleton(注意:是"每容器单例",不是"每 JVM 单例")

· MyBatis ErrorContext(ThreadLocal 实现,其实更接近"线程单例")

收益

· 节省内存与重复创建成本,提供统一访问点

· 避免资源争抢,状态集中可控

代价与注意事项

· 本质是"带状态的全局变量",隐藏类间依赖,单元测试互相污染

· 违反单一职责(既管业务又管自己的生命周期)

· 在集群 / 多类加载器环境下会失效,分布式场景需改用分布式锁或共享存储

和相似模式的区别

现代 Java 项目的建议:优先交给 IoC 容器(Spring)管理生命周期,业务代码里几乎不需要手写 getInstance()。手写单例只适合框架层与 JDK 层代码。

02 工厂方法模式 Factory Method

定义一个创建对象的接口,让子类决定实例化哪一个类 —— 把 new 推迟到子类。

为什么需要它

客户端不该关心"具体创建哪个类",只关心拿到符合接口的对象。工厂方法把变化的创建逻辑封到子类,新增产品时只加类、不改已有代码(开闭原则)。

类结构

图 02 · 工厂方法模式 类结构

Java 实现

// ===== 产品层 =====

publicinterface Notification {

void send(String to, String content);

}

publicclass SmsNotification implements Notification {

@Overridepublicvoid send(String to, String content) {

        System.out.println("[短信] -> " + to + " : " + content);

    }

}

publicclass EmailNotification implements Notification {

@Overridepublicvoid send(String to, String content) {

        System.out.println("[邮件] -> " + to + " : " + content);

    }

}

// ===== 创建者层:工厂方法 create() 延迟到子类实现 =====

publicabstractclass NotificationFactory {

/** 工厂方法:子类决定造什么 */

protectedabstract Notification create();

/** 通用业务流程,与具体产品完全解耦 */

publicfinalvoid notifyUser(String to, String content) {

        Notification n = create();          // 多态发生在这里

        n.send(to, content);

        System.out.println(">>> 已写入发送日志");

    }

}

publicclass SmsFactory extends NotificationFactory {

@Overrideprotected Notification create() { returnnew SmsNotification(); }

}

publicclass EmailFactory extends NotificationFactory {

@Overrideprotected Notification create() { returnnew EmailNotification(); }

}

// ===== 客户端:只依赖抽象工厂,新增渠道时本文件无需修改 =====

publicclass Demo {

publicstaticvoid main(String[] args) {

        NotificationFactory factory = switch (System.getProperty("channel", "sms")) {

case"email" -> new EmailFactory();

default      -> new SmsFactory();

        };

        factory.notifyUser("138****8888", "您的验证码是 9527");

    }

}

典型使用场景

· 对接多种第三方服务(支付渠道、短信网关、存储 OSS/S3),按配置选择实现

· 框架给使用者留扩展点:你实现接口,框架负责调用

· 对象创建需要复杂准备(读配置、加解密、建连接),但客户端不该看到这些细节

· 日志框架按环境选择不同 Appender

JDK 与主流框架里的出处

· Calendar#getInstance() —— 按 Locale/时区返回 BuddhistCalendar、GregorianCalendar 等子类

· NumberFormat#getInstance()、DateFormat#getTimeInstance()

· java.util.Collection#iterator() —— 每个集合自己决定返回哪种迭代器(经典的工厂方法)

· ResourceBundle#getBundle()、SLF4J LoggerFactory#getLogger()

收益

· 新增产品无需修改既有代码(开闭原则)

· 客户端与具体类解耦;通用逻辑可复用在父类

· 配合 Spring 的 Map<String, Xxx> 注入可彻底消灭 switch

代价与注意事项

· 类数量翻倍(每加一个产品就要加一个工厂)

· 单纯为了 new 一个对象而建工厂是过度设计

和相似模式的区别

与简单工厂的区别:简单工厂在一个类的 if/switch 里判断(非 GoF 模式,改就要改源码);工厂方法把判断分派给子类。实际项目中更常见的折中是 "工厂方法 + 注册表":把实现放进 Map<String, Notification>,由 Spring 自动注入全部实现。

03 抽象工厂模式 Abstract Factory

提供一个创建一系列相关 / 相互依赖对象的接口,而无需指定它们具体的类。

为什么需要它

当产品不是"一个"而是"一族",且族内对象必须配套使用时(Mac 按钮不能配 Windows 复选框),抽象工厂保证了这种约束。切换整套产品族,客户端只改一行。

类结构

图 03 · 抽象工厂模式 类结构

Java 实现

// ===== 产品族:按钮 + 复选框,必须同一风格 =====

publicinterface Button   { void render(); }

publicinterface Checkbox { void check(); }

// ===== 抽象工厂:一次产出"一整族"产品 =====

publicinterface UiFactory {

    Button   createButton();

    Checkbox createCheckbox();

}

// ===== 具体工厂:每个工厂保证族内一致性 =====

publicclass MacFactory implements UiFactory {

@Overridepublic Button   createButton()   { return () -> System.out.println("macOS 风格按钮"); }

@Overridepublic Checkbox createCheckbox() { return () -> System.out.println("macOS 风格复选框"); }

}

publicclass WinFactory implements UiFactory {

@Overridepublic Button   createButton()   { return () -> System.out.println("Windows 风格按钮"); }

@Overridepublic Checkbox createCheckbox() { return () -> System.out.println("Windows 风格复选框"); }

}

// ===== 客户端:完全不知道具体类名,切换主题只换一个工厂 =====

publicclass Application {

privatefinal Button button;

privatefinal Checkbox checkbox;

public Application(UiFactory factory) {       // 依赖注入抽象工厂

this.button   = factory.createButton();

this.checkbox = factory.createCheckbox();

    }

publicvoid render() { button.render(); checkbox.check(); }

}

// 运行期决定主题:读取配置即可,业务代码零改动

UiFactory factory = "mac".equals(osName) ? new MacFactory() : new WinFactory();

new Application(factory).render();

典型使用场景

· 跨平台 UI / 主题皮肤切换(按钮、输入框、弹窗必须成套)

· 多套数据源实现:MySQL 方言族 vs Oracle 方言族(Connection + Statement + ResultSet 必须配套)

· 云厂商 SDK 适配:阿里云族、AWS 族(OSS + 短信 + 监控各自成套)

· 单元测试用 Fake 工厂替换真实工厂(FakeRepository + FakeCache 一族)

JDK 与主流框架里的出处

· javax.xml.parsers.DocumentBuilderFactory#newInstance()

· javax.xml.transform.TransformerFactory#newInstance()

· SAXParserFactory、XPathFactory

· JDBC DriverManager 体系:驱动厂商提供 Connection/Statement/ResultSet 一整套

收益

· 保证产品族一致性(客户端不可能拿到混搭产品)

· 切换产品族极其廉价;符合依赖倒置

· 把"创建"集中管理,便于统一加缓存/池化

代价与注意事项

· 新增"产品种类"(如再加一个 Slider)要改抽象工厂接口 + 所有实现,改动面大

· 类数量膨胀,只适合产品族确实存在约束的场景

和相似模式的区别

工厂方法 vs 抽象工厂:工厂方法抽象的是"一个产品的创建"(纵向扩展靠继承);抽象工厂抽象的是"一组产品的创建"(横向扩展靠替换工厂实例)。若你的产品之间没有任何约束关系,用工厂方法就够了,别硬套抽象工厂。

04 建造者模式 Builder

把复杂对象的构造过程与其表示分离,使同样的构造过程可以创建不同的表示。

为什么需要它

构造函数超过 4 个参数、其中多数可选时,"伸缩构造函数"(重载 N 个构造器)和 setter 都会失控:前者难读,后者让对象处于中间态。Builder 用分步骤 + 集中校验 + 一次性 build()解决。

类结构

图 04 · 建造者模式 类结构

Java 实现

// 场景:HTTP 请求对象 —— 参数多、部分可选、希望不可变

publicfinalclass HttpRequest {

privatefinal String url;                  // 必填

privatefinal String method;               // 必填

privatefinalint timeoutMs;               // 可选

privatefinal Map<String, String> headers; // 可选

private HttpRequest(Builder b) {           // 私有构造,只能由 Builder 创建

this.url = b.url;

this.method = b.method;

this.timeoutMs = b.timeoutMs;

this.headers = Map.copyOf(b.headers);  // 防御性拷贝,保证不可变

    }

publicstatic Builder newBuilder(String url, String method) {

returnnew Builder(url, method);       // 必填参数在 Builder 构造时就强制传入

    }

publicstaticfinalclass Builder {

privatefinal String url;

privatefinal String method;

privateint timeoutMs = 3000;                       // 默认值

privatefinal Map<String, String> headers = new HashMap<>();

private Builder(String url, String method) {

this.url = url; this.method = method;

        }

public Builder timeoutMs(int t) { this.timeoutMs = t; returnthis; }   // 链式返回 this

public Builder header(String k, String v) { headers.put(k, v); returnthis; }

public HttpRequest build() {                        // 校验集中在这里

if (url == null || url.isBlank()) thrownew IllegalArgumentException("url 必填");

if (!Set.of("GET","POST","PUT","DELETE").contains(method))

thrownew IllegalArgumentException("不支持的方法: " + method);

if (timeoutMs <= 0) thrownew IllegalArgumentException("超时时间非法");

returnnew HttpRequest(this);

        }

    }

public String url() { return url; }

}

// 使用:一眼看懂每个参数是什么,且得到的是不可变对象

HttpRequest req = HttpRequest.newBuilder("https://api.example.com/user", "GET")

        .timeoutMs(5000)

        .header("Authorization", "Bearer xxxx")

        .header("Accept", "application/json")

        .build();

典型使用场景

· 参数多且大量可选(>4 个):SQL 查询条件、HTTP 请求、报表筛选条件

· 需要构造不可变对象(final 字段 + 无 setter)

· 对象构造依赖顺序(先校验 → 再装配 → 最后初始化),需要集中校验

· 同一构造流程产出不同表示:导出 Word/PDF/HTML 三种文档

· 测试代码里的测试数据构造(Builder + 默认值,只覆盖关心的字段)

JDK 与主流框架里的出处

· StringBuilder / StringBuffer#append()(经典链式建造)

· java.net.http.HttpRequest#newBuilder()(Java 11 官方 HTTP Client)

· Locale.Builder、ProcessBuilder

· Lombok @Builder、OkHttp Request.Builder、MyBatis SqlSessionFactoryBuilder

收益

· 可读性强:new User("a",1,true,null,null) → User.builder().name("a").age(1).vip(true).build()

· 支持默认值、支持校验、可产出不可变对象

· 可逐步构造,适合"先攒参数、最后提交"的场景

代价与注意事项

· 代码量增加(每个类要写一个静态内部 Builder)

· 对象必须先建 Builder,内存/性能有轻微额外开销(可用 Lombok 自动生成)

和相似模式的区别

Builder 与工厂模式的差异:工厂是"一步产出",强调创建哪个类;Builder 是"多步装配",强调按步骤拼装。如果关心的重点是"填参数",用 Builder;是"选实现",用工厂。Lombok @Builder 生成的就是"精简版 Builder"——没有 Director,也没有抽象 Builder 接口,这在 Java 项目里是主流用法。

05 原型模式 Prototype

用原型实例指定创建对象的种类,通过"复制"来创建新对象,而不是 new。

为什么需要它

当 new 一个对象代价很高(要查库、要读远程配置、要解析大文件),而新对象和已有对象差别很小时,直接拷贝内存远比重新构造划算。

类结构

图 05 · 原型模式 类结构

Java 实现

publicclass ReportTemplate implements Cloneable {

private String title;

private List<String> rows = new ArrayList<>();   // 引用类型:浅拷贝会出大坑

public ReportTemplate(String title) {

this.title = title;

        loadHeavyConfigFromDb();                     // 昂贵的初始化:只做一次

    }

privatevoid loadHeavyConfigFromDb() {

        rows.add("【固定表头】");                     // 模拟从数据库加载模板

    }

publicvoid addRow(String r) { rows.add(r); }

/** 原型方法:深拷贝 */

@Override

public ReportTemplate clone() {

try {

            ReportTemplate c = (ReportTemplate) super.clone();  // Object.clone() 是浅拷贝

            c.rows = new ArrayList<>(this.rows);                 // ★ 手动复制可变引用字段

return c;

        } catch (CloneNotSupportedException e) {

thrownew AssertionError(e);

        }

    }

// getter / setter 省略

}

// 原型注册表:预先把昂贵对象造好,按需"复印"

class PrototypeRegistry {

privatestaticfinal Map<String, ReportTemplate> CACHE = new ConcurrentHashMap<>();

static {

        CACHE.put("日报", new ReportTemplate("日报"));

        CACHE.put("周报", new ReportTemplate("周报"));

    }

publicstatic ReportTemplate get(String key) {

        ReportTemplate t = CACHE.get(key);

return t == null ? null : t.clone();       // 每次返回独立副本,互不影响

    }

}

// 使用

ReportTemplate r = PrototypeRegistry.get("日报");

r.addRow("销售额:120 万");

典型使用场景

· 对象创建成本高,但彼此差异小(模板、配置快照、大型图结构)

· 需要保存/恢复对象某一时刻状态(与备忘录模式相近)

· 避免构造时的重复 I/O:一次加载,多次复制

· 游戏 / 图形系统中大量相似实体(小兵、粒子)

JDK 与主流框架里的出处

· Object#clone() + Cloneable 标记接口

· ArrayList#clone()、HashMap#clone()

· Spring 中 prototype 作用域的 Bean(每次 getBean 生成新实例,注意:这是"多例"不是"原型拷贝")

收益

· 绕过构造函数,性能优于重新初始化

· 隐藏创建细节,客户端不关心对象怎么来的

· 运行期动态增删原型(注册表)

代价与注意事项

· 深拷贝陷阱:super.clone() 只做浅拷贝,嵌套引用需要逐层处理,容易漏

· 破坏封装:clone 不走构造器,final 字段、单例语义可能出问题

· 业界普遍认为 Cloneable 设计有缺陷,更推荐用拷贝构造器或序列化/BeanUtils 深拷贝替代

和相似模式的区别

替代方案(实际项目更常用):① 拷贝构造器:new ReportTemplate(other) —— 明确、可控、支持 final 字段;② 静态工厂:ReportTemplate.copyOf(other);③ 序列化(Apache Commons Lang SerializationUtils.clone())或 JSON 序列化实现通用深拷贝。

第二篇 结构型(7 种)

这一组解决的是「对象怎么组合」——用组合代替继承,让结构灵活可拆。

06 适配器模式 Adapter / Wrapper

把一个类的接口转换成客户端期望的另一个接口,使原本不兼容的类可以一起工作。

为什么需要它

系统里总有不兼容的东西:老模块的接口、第三方 SDK、遗留 RPC。适配器就是那个"转接头",让客户端可以统一面向目标接口编程,而不用到处写兼容代码。它是典型的事后补救手段——错误已经发生,用它来止血。

类结构

图 06 · 适配器模式 类结构

Java 实现

// ===== 目标接口:客户端(新系统)期望的样子 =====

publicinterface Logger {

void info(String msg);

void error(String msg);

}

// ===== 被适配者:老系统/第三方库,接口不兼容,且我们无法修改它的源码 =====

class LegacyLogger {

publicvoid log(int level, String message) {

        System.out.println("[legacy level=" + level + "] " + message);

    }

}

// ===== 对象适配器(推荐):组合被适配者,不破坏继承体系 =====

publicclass LoggerAdapter implements Logger {

privatefinal LegacyLogger legacy;              // ★ 组合优于继承

public LoggerAdapter(LegacyLogger legacy) { this.legacy = legacy; }

@Overridepublicvoid info(String msg)  { legacy.log(1, msg); }   // 语义映射:info -> level 1

@Overridepublicvoid error(String msg) { legacy.log(3, msg); }

}

// ===== 类适配器(继承版):Java 单继承限制,使用场景较少 =====

class LoggerAdapter2 extends LegacyLogger implements Logger {

@Overridepublicvoid info(String msg)  { log(1, msg); }

@Overridepublicvoid error(String msg) { log(3, msg); }

}

// ===== 客户端:只认识 Logger,完全不知道 LegacyLogger 存在 =====

Logger log = new LoggerAdapter(new LegacyLogger());

log.info("服务启动完成");

log.error("数据库连接失败");

典型使用场景

· 系统重构 / 老接口升级:新模块用新接口,老实现通过适配器接入

· 对接多家第三方 SDK(多家支付、多家物流),统一成本地接口

· 把旧枚举 / 旧参数模型映射为新模型

· 统一多个类的接口,让它们都能被同一段逻辑处理(如 Spring 的 HandlerAdapter 适配 Servlet、Controller、HttpRequestHandler)

JDK 与主流框架里的出处

· Arrays.asList(T...) —— 把数组"适配"成 List

· InputStreamReader / OutputStreamWriter —— 字节流 ↔ 字符流的适配器

· Collections.list(Enumeration)、Collections.enumeration(Collection) —— 新老集合接口互转

· Spring MVC HandlerAdapter、AOP AdvisorAdapter

· Executors.callable(Runnable) —— 把 Runnable 适配成 Callable

收益

· 复用已有类,无需改动其源码(单一职责 + 开闭原则)

· 客户端与具体实现解耦,切换实现只换适配器

· 一个适配器可适配多个被适配者

代价与注意事项

· 过多适配器会让系统"绕",增加理解成本

· 如果能在设计阶段就统一接口,就不需要适配器 —— 它是补丁,不是目标

和相似模式的区别

对象适配器(组合)vs 类适配器(继承):Java 中优先用对象适配器,因为它不占用继承位、还能适配多个对象。与桥接的区别:适配器是"事后让两个已有接口能合作";桥接是"事前把抽象与实现拆开,让两者独立演化"。

07 桥接模式 Bridge

将抽象部分与其实现部分分离,使它们可以独立地变化 —— 用组合代替多层继承。

为什么需要它

当对象有两个(或以上)独立变化的维度时,纯继承会类爆炸:3 种消息类型 × 3 种发送渠道 = 9 个类,再加一种类型就要加 3 个类。桥接把两个维度拆成两条继承链,用组合(桥)连接,类数变成 3 + 3。

类结构

图 07 · 桥接模式 类结构

Java 实现

// ===== 维度一:实现化角色 —— "怎么发" =====

publicinterface MessageSender {

void send(String title, String body);

}

publicclass SmsSender    implements MessageSender {

publicvoid send(String t, String b) { System.out.println("[短信] " + t + ":" + b); }

}

publicclass EmailSender  implements MessageSender {

publicvoid send(String t, String b) { System.out.println("[邮件] " + t + ":" + b); }

}

publicclass WechatSender implements MessageSender {

publicvoid send(String t, String b) { System.out.println("[企微] " + t + ":" + b); }

}

// ===== 维度二:抽象化角色 —— "发什么等级" =====

publicabstractclass Message {

protectedfinal MessageSender sender;            // ★ 这就是"桥":抽象持有实现的抽象

protected Message(MessageSender sender) { this.sender = sender; }

publicabstractvoid send(String to, String content);

}

publicclass CommonMessage extends Message {

public CommonMessage(MessageSender s) { super(s); }

@Overridepublicvoid send(String to, String content) {

        sender.send("普通消息", "致 " + to + ":" + content);

    }

}

publicclass UrgentMessage extends Message {

public UrgentMessage(MessageSender s) { super(s); }

@Overridepublicvoid send(String to, String content) {

        sender.send("【加急】", "致 " + to + ":" + content + "(请立即处理)");

    }

}

// ===== 两个维度自由组合:3×3 = 9 种行为,只用了 6 个类 =====

new UrgentMessage(new WechatSender()).send("张三", "线上 P0 故障");

new CommonMessage(new SmsSender()).send("李四", "您的快递已发货");

典型使用场景

· 两个维度独立扩展:消息类型 × 发送渠道、支付方式 × 优惠类型、报表格式 × 数据来源

· 需要在运行期切换实现(JDBC 换驱动、渲染引擎换后端)

· 避免多层继承导致的类爆炸

· 抽象与实现都需要被继承扩展的场景

JDK 与主流框架里的出处

· JDBC:DriverManager + 各厂商 Driver —— 同一套 API 桥接到 MySQL/Oracle/PG 实现

· AWT:Component 与 ComponentPeer(抽象组件 ↔ 平台原生实现)

· 日志框架:SLF4J API(抽象)桥接到 Logback / Log4j2(实现),中间还有 log4j-over-slf4j 这类桥接包

收益

· 抽象与实现解耦,各自扩展互不影响;显著减少子类数量

· 实现细节对客户端完全透明,可运行期替换

· 符合组合复用原则与开闭原则

代价与注意事项

· 增加了系统理解难度:需要识别出"两个独立变化的维度"才能正确拆分

· 维度识别错了就是过度设计(只有一个维度时,用普通继承即可)

和相似模式的区别

桥接 vs 适配器:两者结构极其相似(都是"持有一个接口引用"),区别只在时机与意图——桥接是设计之初主动拆分(事前),适配器是接口已不兼容时被动补救(事后)。桥接 vs 抽象工厂:抽象工厂关注的是"创建哪一族产品",桥接关注的是"已创建的实现如何被抽象层使用",二者常配合使用(抽象工厂负责造出具体的 Implementor)。

08 组合模式 Composite

把对象组合成树形结构以表示"部分-整体"的层次,使客户端对单个对象和组合对象的使用具有一致性。

为什么需要它

树形结构(菜单、部门、权限、AST)天然需要"递归处理"。组合模式让客户端不必区分叶子和树枝,用同一段代码遍历整棵树 —— 这正是 if (node instanceof Leaf) 满天飞的解药。

类结构

图 08 · 组合模式 类结构

Java 实现

publicabstractclass Entry {

protectedfinal String name;

protected Entry(String name) { this.name = name; }

publicabstractint size();

publicabstractvoid print(String prefix);

// 透明式:把组合对象的方法定义在抽象层,叶子不支持就抛异常

public Entry add(Entry e) {

thrownew UnsupportedOperationException(name + " 是叶子节点,不支持添加");

    }

}

class File extends Entry {

privatefinalint size;

public File(String name, int size) { super(name); this.size = size; }

@Overridepublicint size() { return size; }

@Overridepublicvoid print(String prefix) { System.out.println(prefix + "📄 " + name + " (" + size + "B)"); }

}

class Directory extends Entry {

privatefinal List<Entry> children = new ArrayList<>();

public Directory(String name) { super(name); }

@Overridepublic Entry add(Entry e) { children.add(e); returnthis; }   // 覆盖父类默认异常

@Overridepublicint size() { return children.stream().mapToInt(Entry::size).sum(); }  // 递归

@Overridepublicvoid print(String prefix) {

        System.out.println(prefix + "📁 " + name + "/ (" + size() + "B)");

        children.forEach(c -> c.print(prefix + "   "));                     // 递归遍历

    }

}

// 使用:客户端完全不区分文件与目录

Directory root = new Directory("root");

root.add(new File("README.md", 2048));

Directory src = new Directory("src");

src.add(new File("Main.java", 1024));

root.add(src);

root.print("");

典型使用场景

· 组织架构 / 部门树 / 员工汇报关系

· 菜单与权限树(含按钮级权限)

· 文件系统抽象、XML/JSON 文档对象模型

· 表达式与规则树((A && B) || C)

· GUI 容器与控件嵌套

JDK 与主流框架里的出处

· java.awt.Container / Component:容器里可以放组件,也可以放容器

· MyBatis SqlNode 体系:IfSqlNode、WhereSqlNode、TextSqlNode 递归拼 SQL

· Jackson JsonNode(ObjectNode / ArrayNode / ValueNode)

· Spring CompositeCacheManager、CompositeMessageSource

收益

· 客户端代码简洁:一套逻辑处理整棵树,无需类型判断

· 新增节点类型非常容易(开闭原则)

· 天然支持递归与层次化操作

代价与注意事项

· 透明式(抽象层声明 add/remove)会让叶子节点背负无意义的方法,只能抛异常 —— 类型不安全

· 安全式(只在 Composite 声明 add)则客户端必须区分类型,失去一致性

· 树太深时递归有栈溢出风险,且难以限制节点类型(如"部门下不能加菜单")

和相似模式的区别

现实中多数项目用安全式 + 泛型/接口约束的折中:抽象层只放公共业务方法,组合操作放在 Composite 里,同时用 type 字段限制可挂载的子节点类型。

09 装饰器模式 Decorator / Wrapper

动态地给一个对象增加额外职责,比生成子类更灵活 —— 可层层叠加,且对客户端透明。

为什么需要它

想给对象加功能,继承会产生类爆炸(A、B、AB、ABC……),且是编译期静态的。装饰器用"同接口 + 持有上一层引用"的方式实现运行期动态叠加:想加什么就包一层,顺序还能决定语义。

类结构

图 09 · 装饰器模式 类结构

Java 实现

// ===== 抽象组件 =====

publicinterface Notifier { void send(String msg); }

// ===== 具体组件:最基础的实现 =====

publicclass BaseNotifier implements Notifier {

@Overridepublicvoid send(String msg) { System.out.println("站内信 → " + msg); }

}

// ===== 装饰器基类:实现同一接口 + 持有被装饰对象 =====

publicabstractclass NotifierDecorator implements Notifier {

protectedfinal Notifier delegate;

protected NotifierDecorator(Notifier delegate) { this.delegate = delegate; }

@Overridepublicvoid send(String msg) { delegate.send(msg); }      // 默认透传

}

// ===== 具体装饰器:各加一种职责 =====

publicclass SmsDecorator extends NotifierDecorator {

public SmsDecorator(Notifier d) { super(d); }

@Overridepublicvoid send(String msg) {

super.send(msg);                                   // 先执行被装饰者的逻辑

        System.out.println("短信 → " + msg);                // 后置增强

    }

}

publicclass WechatDecorator extends NotifierDecorator {

public WechatDecorator(Notifier d) { super(d); }

@Overridepublicvoid send(String msg) { super.send(msg); System.out.println("企微 → " + msg); }

}

publicclass EncryptDecorator extends NotifierDecorator {

public EncryptDecorator(Notifier d) { super(d); }

@Overridepublicvoid send(String msg) { super.send("[加密]" + msg); }   // 前置增强

}

// ===== 自由叠加,包装顺序 = 执行顺序 =====

Notifier n = new EncryptDecorator(

new SmsDecorator(

new WechatDecorator(

new BaseNotifier())));

n.send("您的订单已支付");

// 输出:站内信 → [加密]您的订单已支付 / 企微 → … / 短信 → …

典型使用场景

· IO 流功能叠加(缓冲、压缩、加密、序列化)

· Web 请求/响应包装:给 Request 加解密、加 XSS 过滤(ServletRequestWrapper)

· 缓存层叠加:MyBatis 二级缓存 = 同步 + LRU + FIFO + 日志 多条装饰链

· 给集合加线程安全 / 只读约束

· 在不改动第三方类的前提下增强其行为

JDK 与主流框架里的出处

· java.io 全家桶:new BufferedInputStream(new FileInputStream(f))、GZIPOutputStream、ObjectInputStream

· Collections.synchronizedList()、Collections.unmodifiableList()

· Spring HttpServletRequestWrapper / HttpServletResponseWrapper

· MyBatis Cache 装饰器链:SynchronizedCache → LoggingCache → LruCache → PerpetualCache

收益

· 比继承灵活:运行期动态组合,可随时增删职责

· 避免类爆炸,N 种职责可任意排列组合

· 符合开闭原则:加新装饰器不改旧代码

代价与注意事项

· 会产生大量小类,且包装层数多了调试困难(调用栈很深)

· 装饰顺序敏感,顺序错了语义就错

· 与代理模式结构相似,容易被误用为代理

和相似模式的区别

装饰器 vs 代理:装饰器增强职责,客户端通常知道自己传了什么、可以层层叠加;代理控制访问,客户端往往以为自己在用真实对象,且一般只包一层。装饰器 vs 适配器:装饰器不改变接口,适配器改变接口。

10 外观模式 Facade

为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。

为什么需要它

微服务/分层架构下,一个业务动作往往要串起 5 个服务。让调用方自己拼装,等于把复杂度转嫁给每个客户端。外观提供一个"一句话入口",把编排、事务、回滚收在一处。

类结构

图 10 · 外观模式 类结构

Java 实现

// 客户端只需面对这一个门面:下单 = 锁库存 + 支付 + 建物流 + 通知 + 失败回滚

publicclass OrderFacade {

privatefinal InventoryService inventory;

privatefinal PaymentService   payment;

privatefinal LogisticsService logistics;

privatefinal NotifyService    notify;

public OrderFacade(InventoryService i, PaymentService p,

                       LogisticsService l, NotifyService n) {

this.inventory = i; this.payment = p; this.logistics = l; this.notify = n;

    }

public String placeOrder(Long userId, Long skuId, int qty) {

        inventory.lock(skuId, qty);                             // 1 锁库存

        String orderNo = "NO" + System.nanoTime();

try {

            payment.pay(userId, orderNo, skuId, qty);           // 2 支付

            logistics.createShipment(orderNo, userId);           // 3 建物流单

            notify.send(userId, "下单成功:" + orderNo);          // 4 通知

return orderNo;

        } catch (Exception e) {

            inventory.unlock(skuId, qty);                        // 5 失败补偿

thrownew IllegalStateException("下单失败,已回滚库存", e);

        }

    }

}

// 客户端:一行代码完成原本需要了解 4 个服务才能做的事

String orderNo = orderFacade.placeOrder(1001L, 8823L, 2);

典型使用场景

· Service 层封装多个 DAO / 多个微服务调用,Controller 只调一个方法

· API 网关 / BFF 层聚合多个后端接口,给前端一个响应

· 封装复杂的第三方 SDK(如阿里云 OSS 上传要走 5 步)

· 给遗留系统套一层干净接口,作为重构的过渡层

JDK 与主流框架里的出处

· SLF4J:一个 Logger 接口统一 Logback / Log4j / JUL(外观 + 桥接)

· Tomcat RequestFacade / ResponseFacade(外观 + 安全包装,防止向下转型拿到内部对象)

· Spring JdbcTemplate:把"获取连接→建语句→执行→关连接→异常处理"封装成一个 query()(外观 + 模板方法)

收益

· 大幅降低客户端与子系统的耦合,客户端代码显著变简

· 子系统的重构、替换不影响客户端(只要外观接口不变)

· 分层清晰,是 DDD 应用层 / 防腐层(ACL)的常见实现

代价与注意事项

· 外观容易膨胀成"上帝类"(God Class),什么都往里塞

· 过度封装会让特殊需求无法触达底层,只能再开后门

· 与中介者混淆:外观是单向简化,子系统不知道外观存在

和相似模式的区别

外观 vs 适配器:外观的意图是简化(提供新接口),可以封装多个类;适配器的意图是转换(复用旧接口),通常只包装一个类。实践建议:外观层不要加业务逻辑,只做编排;业务规则应留在领域层。

11 享元模式 Flyweight

运用共享技术有效地支持大量细粒度对象的复用 —— 分离内部状态与外部状态。

为什么需要它

要画 100 万棵树,若每棵树一个对象,内存爆炸。但树的种类只有 3 种(名称/颜色/纹理 = 内部状态,可共享),不同的是坐标(外部状态,由客户端传入)。享元把可共享的部分抽出复用,只留不可共享的部分在外部。

类结构

图 11 · 享元模式 类结构

Java 实现

// ===== 内部状态:可共享、不可变 =====

publicfinalclass TreeType {

privatefinal String name, color, texture;      // 全部 final:享元对象必须是不可变的

public TreeType(String name, String color, String texture) {

this.name = name; this.color = color; this.texture = texture;

    }

/** 外部状态(x, y)由方法参数传入,绝不存进享元对象 */

publicvoid draw(int x, int y) {

        System.out.printf("在 (%d,%d) 绘制 %s 树,颜色=%s,纹理=%s%n", x, y, name, color, texture);

    }

}

// ===== 享元工厂:缓存并复用内部状态对象 =====

publicclass TreeTypeFactory {

privatestaticfinal Map<String, TreeType> CACHE = new ConcurrentHashMap<>();

publicstatic TreeType of(String name, String color, String texture) {

        String key = name + "|" + color + "|" + texture;

return CACHE.computeIfAbsent(key, k -> new TreeType(name, color, texture));  // 有就复用

    }

publicstaticint cachedTypes() { return CACHE.size(); }

}

// ===== 外部状态:每棵树自己的坐标,不共享 =====

publicrecord Tree(int x, int y, TreeType type) {

publicvoid draw() { type.draw(x, y); }

}

// 100 万棵树,但 TreeType 只有 3 个对象

List<Tree> forest = new ArrayList<>();

for (int i = 0; i < 1_000_000; i++) {

    forest.add(new Tree(randomX(), randomY(), TreeTypeFactory.of("杨树", "绿", "粗糙")));

}

System.out.println("享元对象数量 = " + TreeTypeFactory.cachedTypes());   // 1

典型使用场景

· 海量细粒度对象:游戏中的粒子/棋子/树木、报表中的单元格样式

· 文本编辑器中的字符样式(字体、字号共享,位置不共享)

· 池化技术:数据库连接池、线程池、对象池

· 需要大量重复常量对象时避免重复创建

JDK 与主流框架里的出处

· Integer.valueOf(int):-128~127 走 IntegerCache,超出才 new(Integer a=127,b=127; a==b 为 true,128 则为 false)

· Boolean.valueOf()、Character 缓存、Byte/Short/Long 缓存

· String 字符串常量池与 intern()

· 线程池 ThreadPoolExecutor 复用 Worker、数据库连接池复用 Connection

收益

· 大幅减少对象数量,降低内存占用与 GC 压力

· 共享对象集中管理,便于统一维护

代价与注意事项

· 必须严格区分内部/外部状态,设计复杂度上升

· 外部状态的计算可能从"内存"转移到"时间"(用计算换存储)

· 享元对象必须不可变,多线程下才安全

和相似模式的区别

享元 vs 单例:单例强调"全局只能有一个实例";享元强调"共享内部状态",可以有多个不同的享元实例(红棋子、黑棋子各一个)。享元 vs 对象池:对象池中的对象同一时刻只被一个客户端独占;享元对象被所有客户端共享,因此不能保存上下文。

12 代理模式 Proxy

为其他对象提供一种代理以控制对这个对象的访问 —— 客户端无感知地增加横切逻辑。

为什么需要它

想在方法前后统一加日志、事务、权限、缓存、限流,但不想污染业务代码。代理在客户端与真实对象之间插入一个同接口的替身,客户端以为自己在用真实对象,实际所有调用都被代理"截胡"。这是 Spring AOP 的全部秘密。

类结构

图 12 · 代理模式 类结构

Java 实现

// ===== 抽象主题 =====

publicinterface UserService { void save(String name); }

publicclass UserServiceImpl implements UserService {

@Overridepublicvoid save(String name) { System.out.println("保存用户:" + name); }

}

// ===== ① 静态代理:编译期就写好的代理类 =====

publicclass UserServiceProxy implements UserService {

privatefinal UserService target;

public UserServiceProxy(UserService target) { this.target = target; }

@Overridepublicvoid save(String name) {

long s = System.currentTimeMillis();

        System.out.println("[日志] save 开始");

        target.save(name);                                       // 委派给真实对象

        System.out.println("[日志] save 结束,耗时 " + (System.currentTimeMillis() - s) + "ms");

    }

}

// ===== ② JDK 动态代理:一个 Handler 代理任意接口,Spring AOP 的基石 =====

publicclass TimingHandler implements InvocationHandler {

privatefinal Object target;

public TimingHandler(Object target) { this.target = target; }

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("[日志] " + method.getName() + " 开始");

try {

return method.invoke(target, args);                  // 反射调用真实方法

        } finally {

            System.out.println("[日志] " + method.getName() + " 结束");

        }

    }

}

UserService proxy = (UserService) Proxy.newProxyInstance(

        UserService.class.getClassLoader(),

new Class<?>[]{ UserService.class },                    // 只能代理接口

new TimingHandler(new UserServiceImpl()));

proxy.save("张三");

// ③ CGLIB:通过生成子类字节码增强,可代理无接口的类,但不能代理 final 类/方法

典型使用场景

· AOP 横切逻辑:事务、日志、鉴权、监控埋点、幂等

· RPC 客户端 Stub:Feign / Dubbo 消费者拿到的"接口实现"其实是代理

· MyBatis Mapper:只有接口没有实现类,由 MapperProxy 生成代理执行 SQL

· 懒加载:Hibernate 延迟关联对象,真正访问时才查库

· 保护代理(权限校验)、缓存代理(结果缓存)、智能引用(计数/加锁)

JDK 与主流框架里的出处

· java.lang.reflect.Proxy + InvocationHandler

· Spring AOP:JdkDynamicAopProxy / ObjenesisCglibAopProxy(Boot 默认 proxyTargetClass=true,即优先 CGLIB)

· MyBatis MapperProxy

· java.rmi.* 远程代理、Collections.unmodifiableXxx(保护代理)

收益

· 业务代码零侵入,横切逻辑集中维护(单一职责)

· 真实对象可以被保护、延迟创建、远程化

· 静态代理直观;动态代理一个 Handler 服务所有接口

代价与注意事项

· JDK 动态代理只能代理接口,且同类内部方法互调不会经过代理(Spring 事务失效的经典坑)

· CGLIB 不能代理 final 类/方法,且首次生成字节码较慢

· 代理层数多时,异常栈很深,排障困难

和相似模式的区别

代理 vs 装饰器:结构几乎一样,区别在于意图与控制力——代理通常控制访问(决定要不要调、能不能调),客户端往往不知道有代理;装饰器增强行为,通常由客户端主动层层包装。经典坑:Spring 中 this.method() 自调用会绕过代理导致 @Transactional 失效,解决方式是注入自身代理或用 AopContext.currentProxy()。

第三篇 行为型(11 种)

这一组解决的是「对象怎么交互」——让职责分配清晰、通信可控。

13 责任链模式 Chain of Responsibility

把请求的发送者与接收者解耦:让多个对象都有机会处理请求,沿着链传递直到被处理。

为什么需要它

一个请求可能被多个处理器中的某一个处理,但事先不知道是哪一个,而且这个映射关系会经常变(审批金额门槛、风控规则)。责任链把"谁来处理"变成运行期可配置的链条,彻底消灭 if (amount < 1000) ... else if ...。

类结构

图 13 · 责任链模式 类结构

Java 实现

publicrecord ExpenseRequest(String name, int amount) {}

// ===== 抽象处理器 =====

publicabstractclass ApprovalHandler {

private ApprovalHandler next;

/** 链式组装:返回 next 方便连续 .then() */

public ApprovalHandler then(ApprovalHandler next) { this.next = next; return next; }

/** 模板式的处理入口:能处理就处理并终止,否则交给下一位 */

publicvoid handle(ExpenseRequest req) {

if (canHandle(req)) {

            doHandle(req);

return;                                    // 纯责任链:处理即终止

        }

if (next != null) {

            next.handle(req);

        } else {

            System.out.println("⚠ 无人可审批:" + req.name() + " 报销 " + req.amount() + " 元");

        }

    }

protectedabstractboolean canHandle(ExpenseRequest req);

protectedabstractvoid doHandle(ExpenseRequest req);

}

// ===== 具体处理器:各自只关心自己的门槛 =====

class TeamLeader extends ApprovalHandler {

protectedboolean canHandle(ExpenseRequest r) { return r.amount() <= 1000; }

protectedvoid doHandle(ExpenseRequest r) { System.out.println("组长审批通过:" + r.amount()); }

}

class Manager extends ApprovalHandler {

protectedboolean canHandle(ExpenseRequest r) { return r.amount() <= 5000; }

protectedvoid doHandle(ExpenseRequest r) { System.out.println("经理审批通过:" + r.amount()); }

}

class Cfo extends ApprovalHandler {

protectedboolean canHandle(ExpenseRequest r) { return r.amount() <= 50000; }

protectedvoid doHandle(ExpenseRequest r) { System.out.println("财务总监审批通过:" + r.amount()); }

}

class Ceo extends ApprovalHandler {

protectedboolean canHandle(ExpenseRequest r) { returntrue; }          // 兜底

protectedvoid doHandle(ExpenseRequest r) { System.out.println("CEO 审批通过:" + r.amount()); }

}

// ===== 组装链条(顺序即优先级),新增审批层级不影响已有类 =====

ApprovalHandler chain = new TeamLeader();

chain.then(new Manager()).then(new Cfo()).then(new Ceo());

chain.handle(new ExpenseRequest("张三", 12000));   // 财务总监审批通过:12000

典型使用场景

· 审批流 / 工单流转:按金额、按角色逐级上报

· Web 过滤器链:编码、鉴权、限流、日志依次执行(不纯责任链,每个都处理后继续传递)

· 风控规则链:黑名单 → 频次 → 额度 → 人工审核

· 多级缓存查找:本地缓存 → Redis → DB

· 异常处理:按异常类型尝试多种降级策略

JDK 与主流框架里的出处

· javax.servlet.Filter#doFilter(req, res, chain) —— 不纯责任链,chain.doFilter() 决定是否继续

· Spring Security FilterChainProxy:一条链上挂十几二十个 Filter

· Netty ChannelPipeline:inbound / outbound 双向链

· Sentinel SlotChain、Dubbo Filter、MyBatis InterceptorChain

收益

· 发送者不必知道谁处理,接收者也不必知道全貌 —— 双向解耦

· 链条可运行期动态组合、增删改顺序

· 新增处理器零侵入(开闭原则)

代价与注意事项

· 请求可能无人处理:必须设计兜底节点或明确的拒绝策略

· 链条过长会拖慢性能,且调试时难以定位是哪一环

· 不纯责任链(每个节点都处理)要注意副作用顺序

和相似模式的区别

纯 vs 不纯:纯责任链要求"恰好一个节点处理并终止";不纯责任链允许"每个节点都做点事再往下传",Servlet Filter 就是后者。与状态模式的区别:责任链的节点之间互相不知情(由客户端组装);状态模式的节点知道下一个状态是谁(自己决定迁移)。

14 命令模式 Command

把一个请求封装成对象,从而支持排队、记录日志、撤销/重做等可参数化的操作。

为什么需要它

把"动作"变成对象后,动作就可被存储、传递、排队、重放、撤销。这是把"调用"提升为一等公民:遥控器按钮、任务队列、事务日志、undo 栈,本质都是命令对象。

类结构

图 14 · 命令模式 类结构

Java 实现

// ===== 接收者:真正执行操作的对象 =====

publicclass Light {

publicvoid on()  { System.out.println("灯打开了"); }

publicvoid off() { System.out.println("灯关闭了"); }

}

// ===== 命令接口 =====

publicinterface Command {

void execute();

void undo();                         // 支持撤销是命令模式的杀手锏

}

// ===== 具体命令:把"接收者 + 动作"打包 =====

publicclass LightOnCommand implements Command {

privatefinal Light light;

public LightOnCommand(Light light) { this.light = light; }

@Overridepublicvoid execute() { light.on(); }

@Overridepublicvoid undo()    { light.off(); }       // 撤销 = 反向操作

}

// ===== 调用者:不关心命令内容,只负责触发与历史记录 =====

publicclass RemoteControl {

privatefinal Deque<Command> history = new ArrayDeque<>();   // 撤销栈

publicvoid submit(Command c) { c.execute(); history.push(c); }

publicvoid undoLast() {

if (!history.isEmpty()) history.pop().undo();

    }

}

// ===== 使用 =====

Light light = new Light();

RemoteControl rc = new RemoteControl();

rc.submit(new LightOnCommand(light));   // 灯打开了

rc.undoLast();                          // 灯关闭了

// 进阶:命令队列 —— 把命令丢进线程池异步执行,天然支持重试与削峰

BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

queue.put(() -> new LightOnCommand(light).execute());

典型使用场景

· 撤销 / 重做(编辑器、IDE、图形工具)

· 任务队列与异步执行:线程池里的 Runnable 就是最经典的无撤销命令

· 事务与补偿:每个命令记日志,失败时按日志反向补偿(Saga)

· 宏命令:一次点击执行一串命令(批处理、一键部署)

· 按钮 / 菜单 / 快捷键统一抽象为命令对象

JDK 与主流框架里的出处

· Runnable / Callable:提交给线程池的任务即命令对象

· Swing Action 接口、UndoManager(标准撤销栈实现)

· ThreadPoolExecutor 的 BlockingQueue 就是命令队列

· 消息队列:把命令序列化后跨网络投递(分布式命令模式)

收益

· 发送者与接收者彻底解耦,调用者不需要知道命令细节

· 天然支持撤销、排队、日志、事务、重试

· 易于组合成宏命令

代价与注意事项

· 每个操作都要写一个命令类,类数量明显增加

· 简单场景下显得笨重(一个按钮只做一件事时,直接调用更清晰)

和相似模式的区别

命令 vs 策略:策略关注"怎么做"(替换算法);命令关注"做什么"(封装请求使其可存储、传递、撤销)。与备忘录配合:复杂撤销可以用备忘录保存状态快照,而不必为每个操作写 undo() 方法。

15 解释器模式 Interpreter

给定一个语言,定义它的文法表示,并定义一个解释器来解释语言中的句子。

为什么需要它

当你需要定义一门小语言并反复执行它 —— 规则表达式、查询条件、公式计算 —— 把文法映射成一棵语法树,每个节点是一个表达式对象,递归解释即可。

类结构

图 15 · 解释器模式 类结构

Java 实现

// ===== 上下文:存放变量值 =====

publicclass Context {

privatefinal Map<String, Integer> vars = new HashMap<>();

publicvoid put(String k, int v) { vars.put(k, v); }

publicint lookup(String k) { return vars.getOrDefault(k, 0); }

}

// ===== 抽象表达式 =====

publicinterface Expression { int interpret(Context ctx); }

// ===== 终结符表达式 =====

record NumberExpression(int value) implements Expression {

publicint interpret(Context ctx) { return value; }

}

record VariableExpression(String name) implements Expression {

publicint interpret(Context ctx) { return ctx.lookup(name); }

}

// ===== 非终结符表达式:由子表达式递归组合 =====

record AddExpression(Expression left, Expression right) implements Expression {

publicint interpret(Context ctx) { return left.interpret(ctx) + right.interpret(ctx); }

}

record SubExpression(Expression left, Expression right) implements Expression {

publicint interpret(Context ctx) { return left.interpret(ctx) - right.interpret(ctx); }

}

// ===== 解释 "a + b - 10" =====

Expression expr = new SubExpression(

new AddExpression(new VariableExpression("a"), new VariableExpression("b")),

new NumberExpression(10));

Context ctx = new Context();

ctx.put("a", 30); ctx.put("b", 5);

System.out.println(expr.interpret(ctx));     // 25

典型使用场景

· 业务规则引擎:风控规则、促销规则("满 100 减 20 且 会员等级>3")

· 自定义查询条件 DSL、报表公式计算

· SQL / HQL / EL 表达式解析

· 配置化条件判断(工作流网关表达式)

JDK 与主流框架里的出处

· java.util.regex.Pattern / Matcher:正则表达式的解释器

· Spring ExpressionParser(SpEL)、javax.el EL 表达式

· java.text.Format、MessageFormat 解析占位符

· Hibernate HQL、MyBatis OGNL 表达式

收益

· 文法易于扩展:新增一种语法规则只需加一个表达式类

· 语法树结构清晰,便于调试与优化

· 把"易变的规则"从代码中剥离出来,支持配置化

代价与注意事项

· 语法复杂时类会爆炸(每条规则一个类),维护成本高

· 递归解释执行效率低于编译执行,不适合高性能热路径

· 实际工业级解析器(SQL、JSON)通常用 ANTLR / JavaCC 生成,而非手写解释器

和相似模式的区别

实践建议:只有语法规则简单且稳定(几十条以内)才手写解释器。复杂场景直接用 ANTLR、Spring EL、Aviator、QLExpress 等成熟表达式引擎。与组合模式的关系:抽象语法树本身就是组合模式,解释器模式 = 组合模式 + 解释行为。

16 迭代器模式 Iterator

提供一种方法顺序访问聚合对象中的各个元素,而又不暴露其内部的表示。

为什么需要它

集合的内部结构千差万别(数组、链表、树、哈希表、文件流),如果客户端要针对每种结构写不同的遍历代码,就会被"实现细节"污染。迭代器提供统一的遍历协议:hasNext() + next()。

类结构

图 16 · 迭代器模式 类结构

Java 实现

publicrecord Book(String name) {}

// ===== 聚合对象:实现 Iterable 即可用 for-each =====

publicclass BookShelf implements Iterable<Book> {

privatefinal List<Book> books = new ArrayList<>();   // 内部结构对外完全隐藏

publicvoid add(Book b) { books.add(b); }

publicint size() { return books.size(); }

public Book get(int i) { return books.get(i); }

@Overridepublic Iterator<Book> iterator() { return books.iterator(); }

/** 同一个聚合可以提供多种遍历方式:这就是迭代器模式的灵活性 */

public Iterator<Book> reverseIterator() {

returnnew Iterator<>() {

privateint idx = books.size() - 1;

@Overridepublicboolean hasNext() { return idx >= 0; }

@Overridepublic Book next() {

if (!hasNext()) thrownew NoSuchElementException();

return books.get(idx--);

            }

        };

    }

}

// 客户端:完全不关心书架是数组还是链表

BookShelf shelf = new BookShelf();

shelf.add(new Book("设计模式")); shelf.add(new Book("重构"));

for (Book b : shelf) {                       // 编译后就是 iterator() + hasNext() + next()

    System.out.println(b.name());

}

shelf.reverseIterator().forEachRemaining(b -> System.out.println("倒序:" + b.name()));

典型使用场景

· 需要遍历集合但不希望暴露内部结构(List / Set / 树 / 图)

· 同一集合需要提供多种遍历方式:正序、倒序、按条件过滤、深度优先/广度优先

· 统一遍历不同数据结构:分页结果、文件行、数据库 ResultSet

· 需要支持"边遍历边删除"的安全语义(iterator.remove())

JDK 与主流框架里的出处

· java.util.Iterator / Iterable,以及所有集合类的实现

· 增强 for 循环(语法糖,编译为 Iterator 调用)

· Spliterator(Java 8):支持并行遍历,是 Stream 并行流的基石

· Enumeration(遗留接口,已被 Iterator 取代)

· Guava FluentIterable、MyBatis Cursor(流式遍历大结果集)

收益

· 遍历与聚合分离,聚合内部可自由重构(ArrayList 换 LinkedList 客户端无感)

· 同一集合可并行多个迭代器、多种遍历策略

· 符合单一职责:集合管存储,迭代器管遍历

代价与注意事项

· 对某些结构(如树)遍历逻辑复杂,写起来繁琐

· 遍历过程中修改集合会触发 ConcurrentModificationException(fail-fast),需要使用迭代器的 remove() 或并发集合

· 现代 Java 中 Iterable + Stream API 已足够,很少需要手写迭代器

和相似模式的区别

fail-fast vs fail-safe:ArrayList 迭代器是 fail-fast(用 modCount 检测并发修改,快速抛异常);CopyOnWriteArrayList 是 fail-safe(遍历快照,不抛异常但可能读到旧数据)。与访问者模式:迭代器负责"把所有元素取出来",访问者负责"对不同类型的元素做不同操作",两者常结合使用。

17 中介者模式 Mediator

用一个中介对象封装一组对象之间的交互,使这些对象不必显式相互引用,从而降低耦合。

为什么需要它

当 N 个对象两两交互,关系数是 N²;任何一个改动都可能波及所有其它对象。中介者把网状结构改成星型结构:每个人只认识中介者,交互规则集中在中介者一处。

类结构

图 17 · 中介者模式 类结构

Java 实现

// ===== 中介者接口 =====

publicinterface ChatMediator {

void register(User user);

void send(String msg, User from);        // 由中介者决定"这条消息该给谁"

}

// ===== 具体中介者:所有交互规则集中在这里 =====

publicclass ChatRoom implements ChatMediator {

privatefinal List<User> users = new CopyOnWriteArrayList<>();

@Overridepublicvoid register(User u) { users.add(u); }

@Overridepublicvoid send(String msg, User from) {

        users.stream()

             .filter(u -> u != from)                       // 规则:不发给发送者自己

             .forEach(u -> u.receive(from.name() + ":" + msg));

    }

}

// ===== 同事类:只认识中介者,彼此之间零引用 =====

publicabstractclass User {

protectedfinal ChatMediator mediator;

privatefinal String name;

protected User(ChatMediator m, String name) { this.mediator = m; this.name = name; }

public String name() { return name; }

publicvoid send(String msg) { mediator.send(msg, this); }    // 一律交给中介者

publicabstractvoid receive(String msg);

}

publicclass ChatUser extends User {

public ChatUser(ChatMediator m, String name) { super(m, name); }

@Overridepublicvoid receive(String msg) { System.out.println("[" + name() + " 收到] " + msg); }

}

// ===== 使用:加人、改规则都只动 ChatRoom =====

ChatMediator room = new ChatRoom();

User a = new ChatUser(room, "产品"), b = new ChatUser(room, "开发"), c = new ChatUser(room, "测试");

room.register(a); room.register(b); room.register(c);

a.send("需求评审定在明天 10 点");

典型使用场景

· GUI 表单联动:选择省份后城市下拉框跟着变,控件之间不互相引用

· 聊天室 / 群聊消息路由

· 微服务编排与 Saga:编排器作为中介者协调各服务,服务之间不直接调用

· 机场塔台调度:所有飞机只与塔台通信,不与其他飞机协商

· 复杂状态流转(订单 → 库存 → 支付 → 物流)交由编排器统一协调

JDK 与主流框架里的出处

· ExecutorService:任务提交者与执行线程之间不直接通信,由线程池中介

· MessageQueue(Kafka/RocketMQ):分布式中介者,生产者与消费者完全解耦

· java.util.Timer:任务之间不互相感知,统一由 Timer 调度

· MVC 中的 Controller:Model 与 View 不直接通信,Controller 充当协调者

收益

· 把 N² 的网状依赖降为 N,极大降低耦合

· 交互规则集中,便于修改与扩展

· 同事类可独立复用(换个中介者就能换套交互规则)

代价与注意事项

· 中介者容易膨胀为"上帝对象",承载过多逻辑

· 过度集中会让中介者成为性能瓶颈与单点故障

· 与外观混淆:外观是单向简化(客户端→子系统),中介者是双向协调

和相似模式的区别

中介者 vs 观察者:观察者是广播(一对多,发布者不知道订阅者),中介者是定向协调(多对多,中介者知道所有同事)。实践中二者常组合:事件总线 = 观察者 + 中介者,Spring 的 ApplicationEventPublisher 就是典型例子。实践建议:当同事对象超过 4~5 个且交互规则复杂时再引入中介者,否则得不偿失。

18 备忘录模式 Memento

在不破坏封装的前提下,捕获一个对象的内部状态并在之后恢复它。

为什么需要它

要实现撤销、回滚、存档,就必须保存对象某时刻的状态。但如果直接暴露字段去读,就破坏了封装。备忘录的巧妙之处:快照对象对原发器"宽接口"(可读),对外"窄接口"(不可读)。

类结构

图 18 · 备忘录模式 类结构

Java 实现

// ===== 原发器:业务对象,自己负责创建与恢复快照 =====

publicclass Editor {

private String content = "";

publicvoid type(String text) { content += text; }

publicvoid show() { System.out.println("当前内容:" + content); }

public Memento save()     { returnnew Memento(content); }     // 创建快照

publicvoid restore(Memento m) { this.content = m.content(); }  // 恢复快照

}

// ===== 备忘录:不可变快照,对外只暴露"这是一个快照",不暴露可修改接口 =====

publicfinalclass Memento {

privatefinal String content;                 // final + 无 setter

    Memento(String content) { this.content = content; }   // 包级私有构造:外部无法伪造

    String content() { return content; }                  // 包级私有读取:Caretaker 看不到内容

}

// ===== 管理者:只负责保管,从不查看或修改快照内容 =====

publicclass History {

privatefinal Deque<Memento> undoStack = new ArrayDeque<>();

publicvoid push(Memento m) { undoStack.push(m); }

public Memento pop() { return undoStack.isEmpty() ? null : undoStack.pop(); }

}

// ===== 使用 =====

Editor editor = new Editor();

History history = new History();

editor.type("第一版内容");  history.push(editor.save());

editor.type("第二版内容");

editor.show();                                  // 第一版内容第二版内容

editor.restore(history.pop());

editor.show();                                  // 第一版内容

典型使用场景

· 编辑器 / IDE 的撤销重做(Ctrl+Z / Ctrl+Y)

· 游戏存档与读档

· 数据库事务回滚与 Savepoint

· 表单草稿箱、多步向导的"上一步"

· 批量任务失败后的状态回滚、灰度发布的快速回退

JDK 与主流框架里的出处

· Java 标准库没有直接实现,但思想处处可见:Cloneable + 序列化快照(SerializationUtils.clone())

· JDBC Savepoint:事务中的部分回滚点

· java.util.Date 的设计(可变对象的快照常靠拷贝实现)

· 版本控制系统 Git 的 commit 快照本质就是备忘录

收益

· 保存/恢复机制与业务逻辑分离,且不破坏封装

· 快照不可变,天然线程安全;历史栈便于实现多级撤销

· 原发器可以自主决定哪些状态入快照

代价与注意事项

· 内存开销大:频繁对大对象做全量快照会吃爆内存,需要增量快照(只存 diff)或限制栈深度

· 管理者需要感知生命周期,及时清理过期快照

· 深拷贝实现不严谨时会"快照被后续修改污染"

和相似模式的区别

备忘录 vs 命令模式做撤销:命令模式靠"反向操作"回滚(undo() 执行一次相反动作),适合操作可逆且廉价;备忘录靠"状态快照"回滚,适合状态复杂、反向操作难写。二者可混合:命令 + 快照,实现最稳妥的 undo。与原型模式:备忘录常借 clone() 生成快照,但备忘录强调"带版本的历史",原型强调"快速复制"。

19 观察者模式 Observer / Publish-Subscribe

定义对象间一对多的依赖关系:当一个对象状态改变时,所有依赖它的对象都会收到通知并自动更新。

为什么需要它

订单支付后要发短信、发券、记审计、通知仓库…… 如果把这些全写进 pay() 方法里,订单服务就被"下游"绑架了。观察者把这些逻辑倒置为订阅关系:发布者完全不知道谁在监听,新增下游零改动。

类结构

图 19 · 观察者模式 类结构

Java 实现

// ===== 观察者接口 =====

publicinterface Observer { void update(String orderNo); }

// ===== 主题(发布者)=====

publicabstractclass Subject {

// CopyOnWriteArrayList:防止遍历通知时因新增/移除订阅者而抛并发异常

privatefinal List<Observer> observers = new CopyOnWriteArrayList<>();

publicvoid attach(Observer o) { observers.add(o); }

publicvoid detach(Observer o) { observers.remove(o); }

protectedvoid notifyObservers(String orderNo) {

        observers.forEach(o -> o.update(orderNo));       // 推模型:把数据推给订阅者

    }

}

// ===== 具体主题:只负责发通知,不关心谁在处理 =====

publicclass OrderService extends Subject {

publicvoid pay(String orderNo) {

        System.out.println("订单 " + orderNo + " 已支付");

        notifyObservers(orderNo);                        // 一行搞定,后续加功能不改这里

    }

}

// ===== 具体观察者:各自独立演进 =====

class SmsObserver    implements Observer { publicvoid update(String o) { System.out.println("发短信:" + o); } }

class CouponObserver implements Observer { publicvoid update(String o) { System.out.println("发优惠券:" + o); } }

class AuditObserver  implements Observer { publicvoid update(String o) { System.out.println("审计日志:" + o); } }

OrderService svc = new OrderService();

svc.attach(new SmsObserver()); svc.attach(new CouponObserver()); svc.attach(new AuditObserver());

svc.pay("NO20261027");

// ===== Spring 事件版(观察者 + 中介者,生产环境最常用)=====

record OrderPaidEvent(String orderNo) {}

@Component

class SmsListener {

@EventListener// 异步只需再加 @Async

publicvoid on(OrderPaidEvent e) { System.out.println("发短信:" + e.orderNo()); }

}

// 发布

applicationEventPublisher.publishEvent(new OrderPaidEvent("NO20261027"));

典型使用场景

· 订单状态变更后触发下游:短信、积分、物流、对账

· 配置中心推送:配置变更通知所有节点刷新

· 消息通知、站内信、邮件订阅

· GUI 事件监听(按钮点击、文本框变化)

· 跨模块解耦:领域事件驱动架构(EDA)

JDK 与主流框架里的出处

· java.util.Observer / Observable:Java 9 起已废弃,不推荐使用

· java.beans.PropertyChangeListener + PropertyChangeSupport:仍在广泛使用

· Spring ApplicationEventPublisher / @EventListener / ApplicationListener

· Guava EventBus、Reactor(响应式流:观察者模式的超集)

· Servlet HttpSessionListener、MQ 的发布订阅模型

收益

· 发布者与订阅者完全解耦,可独立扩展(开闭原则)

· 支持广播通信,建立一套触发机制非常廉价

· 配合 MQ / 事件总线可平滑演进为分布式事件驱动

代价与注意事项

· 同步通知会拖慢主流程:一个订阅者抛异常/超时可能拖垮发布者(用 @Async 或 MQ 解耦)

· 通知顺序无保障,调试时"到底谁改了状态"难以追踪

· 循环依赖 + 观察者容易造成"事件风暴",出现内存泄漏(订阅者忘了注销)

和相似模式的区别

推模型 vs 拉模型:推模型把数据作为参数传给 update(data)(简单,但订阅者被"喂"固定数据);拉模型把主题自身传过去 update(subject),订阅者按需取(灵活,但要小心并发)。观察者 vs 中介者:观察者是广播、彼此不知情;中介者是定向协调、知道所有同事。事件总线 = 观察者 + 中介者。

20 状态模式 State

允许一个对象在其内部状态改变时改变它的行为 —— 看起来像是修改了它的类。

为什么需要它

当对象的行为取决于它的状态,且状态很多时,代码会充满 if (status == A) ... else if (status == B)。状态模式把每个状态的行为 + 状态迁移规则封装到独立类中,让迁移逻辑就地可见。

类结构

图 20 · 状态模式 类结构

Java 实现

// ===== 状态接口:列出该上下文所有可能的动作 =====

publicinterface OrderState {

void pay(OrderContext ctx);

void ship(OrderContext ctx);

void confirm(OrderContext ctx);

}

// ===== 具体状态:既定义行为,也定义"下一步去哪" =====

class PendingPayState implements OrderState {

@Overridepublicvoid pay(OrderContext ctx) {

        System.out.println("支付成功");

        ctx.setState(new PaidState());                     // 状态迁移写在状态类里

    }

@Overridepublicvoid ship(OrderContext ctx)    { thrownew IllegalStateException("未支付,不能发货"); }

@Overridepublicvoid confirm(OrderContext ctx) { thrownew IllegalStateException("未支付,不能确认收货"); }

}

class PaidState implements OrderState {

@Overridepublicvoid pay(OrderContext ctx) { System.out.println("已支付,请勿重复支付"); }

@Overridepublicvoid ship(OrderContext ctx) {

        System.out.println("发货成功");

        ctx.setState(new ShippedState());

    }

@Overridepublicvoid confirm(OrderContext ctx) { thrownew IllegalStateException("未发货,不能确认收货"); }

}

class ShippedState implements OrderState {

@Overridepublicvoid pay(OrderContext ctx)     { System.out.println("已支付,请勿重复支付"); }

@Overridepublicvoid ship(OrderContext ctx)    { System.out.println("已发货,请勿重复发货"); }

@Overridepublicvoid confirm(OrderContext ctx) {

        System.out.println("确认收货,订单完成");

        ctx.setState(new DoneState());

    }

}

class DoneState implements OrderState {

@Overridepublicvoid pay(OrderContext ctx)     { thrownew IllegalStateException("订单已完成"); }

@Overridepublicvoid ship(OrderContext ctx)    { thrownew IllegalStateException("订单已完成"); }

@Overridepublicvoid confirm(OrderContext ctx) { thrownew IllegalStateException("订单已完成"); }

}

// ===== 上下文:把动作委派给当前状态对象 =====

publicclass OrderContext {

private OrderState state = new PendingPayState();

void setState(OrderState s) { this.state = s; }

publicvoid pay()     { state.pay(this); }          // 无 if-else,全靠多态

publicvoid ship()    { state.ship(this); }

publicvoid confirm() { state.confirm(this); }

}

典型使用场景

· 订单 / 工单 / 审批单状态机(待支付→已支付→已发货→已完成)

· 游戏角色状态(正常/眩晕/无敌/死亡,行为完全不同)

· 设备状态(电梯运行/停止/检修、TCP 连接建立/监听/关闭)

· 水闸/红绿灯等有限状态系统

· 替代复杂的 if-else 状态分支(分支超过 3 个就该考虑)

JDK 与主流框架里的出处

· Thread.State(NEW/RUNNABLE/BLOCKED/WAITING/TERMINATED,枚举化状态)

· Spring StateMachine(官方状态机框架)

· 工作流引擎 Flowable / Activiti 的任务状态流转

· Netty Channel 的连接状态、NIO SelectionKey 状态位

收益

· 消灭庞大的条件分支,新增状态只加类(开闭原则)

· 状态迁移规则集中、显式,可读性远超 if-else

· 状态行为可复用、可测试

代价与注意事项

· 状态多时类数量膨胀(15+ 状态的复杂工单需评估)

· 状态之间互相持有引用可能导致循环依赖

· 若每个状态只在个别方法上不同,接口会逼迫实现一堆空方法(可用适配器基类提供默认实现)

和相似模式的区别

状态 vs 策略:结构一模一样(都是"上下文持有一个接口,运行时切换实现"),区别在意图——① 策略由客户端主动选择算法,策略之间互不感知、无迁移关系;② 状态由对象内部条件自动迁移,状态类互相知道下一个状态是谁。判断标准:如果这些实现类之间存在"下一步去哪"的关系,就是状态;如果只是"换一种算法",就是策略。

21 策略模式 Strategy

定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换 —— 算法的变化独立于使用它的客户端。

为什么需要它

同一个业务动作有多种实现方式(支付渠道、排序算法、折扣规则、文件格式),且需要运行期自由切换。策略模式把"算法"抽成接口,让上下文只依赖抽象 —— 消灭 switch,新增算法零改动。

类结构

图 21 · 策略模式 类结构

Java 实现

// ===== 策略接口 =====

publicinterface PayStrategy {

    String code();                       // 渠道标识:用于自动注册

void pay(BigDecimal amount);

}

@Component("alipay")                     // 每种实现交给 Spring 管理

class AliPayStrategy implements PayStrategy {

public String code() { return"alipay"; }

publicvoid pay(BigDecimal amount) { System.out.println("支付宝支付 " + amount + " 元"); }

}

@Component("wechat")

class WechatPayStrategy implements PayStrategy {

public String code() { return"wechat"; }

publicvoid pay(BigDecimal amount) { System.out.println("微信支付 " + amount + " 元"); }

}

// ===== 上下文:注入全部实现,用 Map 彻底消灭 switch/if-else =====

@Service

publicclass PayService {

privatefinal Map<String, PayStrategy> strategies;

public PayService(List<PayStrategy> list) {          // Spring 会自动注入所有实现类

this.strategies = list.stream()

                .collect(Collectors.toMap(PayStrategy::code, Function.identity()));

    }

publicvoid pay(String channel, BigDecimal amount) {

        PayStrategy s = Optional.ofNullable(strategies.get(channel))

                .orElseThrow(() -> new IllegalArgumentException("不支持的支付渠道:" + channel));

        s.pay(amount);

    }

}

// JDK 中最经典的策略:Comparator —— 排序"算法"由调用方决定

users.sort(Comparator.comparing(User::getAge).reversed());

users.sort(Comparator.comparing(User::getName));        // 换一种策略,仅此一行

典型使用场景

· 多渠道支付 / 多短信网关 / 多对象存储,运行期按配置或用户选择切换

· 促销折扣计算(满减、折扣、会员价、券叠加)

· 排序算法、压缩算法、加密算法可插拔

· 数据导出格式(CSV/Excel/PDF)、文件解析策略

· 风控规则:不同业务线使用不同评分模型

JDK 与主流框架里的出处

· Comparator:最纯粹的策略接口,Collections.sort(list, cmp)

· ThreadPoolExecutor.RejectedExecutionHandler:AbortPolicy / CallerRunsPolicy / DiscardPolicy / DiscardOldestPolicy

· java.util.zip.Deflater 的压缩级别策略

· Spring Resource 体系(ClassPathResource / FileSystemResource / UrlResource)

· MyBatis Executor:SimpleExecutor / ReuseExecutor / BatchExecutor

收益

· 彻底消灭条件分支,新增算法只加类(开闭原则)

· 算法可独立测试、独立复用

· 配合 Spring 的 List<T> 注入,实现"零配置自动注册",是消灭 switch 的标准姿势

代价与注意事项

· 客户端必须知道有哪些策略才能选择(可配合工厂/配置中心解决)

· 策略类数量增加;策略无状态时应做成单例 Bean 避免重复创建

· 所有策略都要实现接口方法,可能导致部分实现出现空方法(可用抽象类提供默认实现)

和相似模式的区别

策略 + 工厂 = 生产环境最常用组合:用 Map<String, Strategy> 做注册表(工厂的变体),彻底消除 switch。策略 vs 模板方法:策略用组合 + 接口(整体替换算法,更灵活);模板方法用继承 + 抽象方法(只替换流程中的某几步,骨架固定)。现代替代:Java 8+ 的简单策略可直接用 Function / lambda 传递,不必为每个算法建类。

22 模板方法模式 Template Method

定义一个操作中的算法骨架,把某些步骤延迟到子类中 —— 子类不改变结构即可重定义某些步骤。

为什么需要它

多个流程骨架相同、细节不同(导入数据:读文件→解析→过滤→入库→失败回滚)。把固定流程用 final 锁在父类,把变化的部分做成抽象方法留给子类 —— 这就是"好莱坞原则":别调用我们,我们会调用你。

类结构

图 22 · 模板方法模式 类结构

Java 实现

publicabstractclass DataImporter {

/** 模板方法:用 final 锁定算法骨架,防止子类改坏流程 */

publicfinal ImportResult importFrom(Path file) {

long start = System.currentTimeMillis();

try {

            List<String> lines  = read(file);        // ① 固定步骤

            List<Record> recs   = parse(lines);      // ② 抽象:子类实现

            recs                = filter(recs);      // ③ 钩子:子类可选覆盖

int count           = save(recs);        // ④ 抽象:子类实现

returnnew ImportResult(count, System.currentTimeMillis() - start, true);

        } catch (Exception e) {

            rollback();                              // ⑤ 固定步骤:统一失败处理

returnnew ImportResult(0, System.currentTimeMillis() - start, false);

        }

    }

protected List<String> read(Path p) throws IOException { return Files.readAllLines(p); }

protectedabstract List<Record> parse(List<String> lines);

protectedabstractint save(List<Record> recs);

/** 钩子方法(Hook):提供默认空实现,子类按需覆盖 */

protected List<Record> filter(List<Record> recs) { return recs; }

protectedvoid rollback() {}

}

// 子类只填差异部分,流程与异常处理完全复用

publicclass CsvUserImporter extends DataImporter {

@Overrideprotected List<Record> parse(List<String> lines) {

return lines.stream().skip(1).map(l -> new Record(l.split(","))).toList();

    }

@Overrideprotectedint save(List<Record> recs) {

        recs.forEach(r -> System.out.println("入库:" + r));

return recs.size();

    }

@Overrideprotected List<Record> filter(List<Record> recs) {   // 覆盖钩子:过滤无效行

return recs.stream().filter(Record::isValid).toList();

    }

}

典型使用场景

· 数据导入/导出流程(读→解析→校验→写→回滚)

· 接口调用模板(鉴权→拼参数→发请求→解析→重试→关资源)

· 测试框架的生命周期(setUp → runTest → tearDown,JUnit 就是这么做的)

· 报表生成:取数 → 计算 → 渲染 → 输出,渲染方式各不同

· 审批/下单等"标准流程 + 差异化细节"的业务

JDK 与主流框架里的出处

· HttpServlet#service():模板方法,调用子类实现的 doGet/doPost

· AbstractQueuedSynchronizer:acquire() 是模板方法,tryAcquire() 由子类(ReentrantLock/Semaphore)实现

· InputStream#read(byte[],int,int) 内部调用子类实现的 read()

· Spring 全家桶:JdbcTemplate、RestTemplate、RedisTemplate、JmsTemplate(模板方法 + 回调 RowMapper)

· JUnit 的 @BeforeEach / @Test / @AfterEach 生命周期

收益

· 封装不变部分、扩展可变部分,是代码复用的经典手段

· 流程集中管控,异常/日志/事务统一处理,子类写不错

· 反向控制(好莱坞原则)让框架代码主导流程

代价与注意事项

· 继承的固有缺点:父类改动会波及所有子类,且 Java 单继承会占用继承位

· 步骤过多时子类被迫实现一堆抽象方法

· 与策略模式相比,运行期无法更换算法(策略是组合,可动态替换)

和相似模式的区别

模板方法 vs 策略:模板方法用继承替换"流程中的某几步",骨架固定、不可运行期更换;策略用组合替换"整个算法",可随时切换。优先选策略(组合优于继承),除非确实需要复用父类中那段固定的流程代码。模板 + 回调(Callback)是更好的折中:Spring 的 JdbcTemplate.execute(PreparedStatementCallback) 把"某一步"作为函数参数传入,既复用流程又不占用继承位。

23 访问者模式 Visitor

表示一个作用于某对象结构各元素的操作 —— 可以在不改变元素类的前提下定义作用于这些元素的新操作。

为什么需要它

当一个结构稳定(元素类型很少变)但需要不断增加新操作时(导出 JSON/XML/Excel、做各种统计),把操作写进元素类会让元素类越来越臃肿。访问者把操作外置,通过双分派实现对不同类型的差异化处理。

类结构

图 23 · 访问者模式 类结构

Java 实现

// ===== 元素接口:只留一个 accept 入口 =====

publicinterface ReportElement { void accept(ReportVisitor v); }

record SalesRow(String region, BigDecimal amount) implements ReportElement {

@Overridepublicvoid accept(ReportVisitor v) { v.visit(this); }   // ★ 第二次分派:this 的静态类型确定重载

}

record RefundRow(String region, BigDecimal amount, String reason) implements ReportElement {

@Overridepublicvoid accept(ReportVisitor v) { v.visit(this); }

}

// ===== 访问者接口:为每种元素各准备一个 visit 重载 =====

publicinterface ReportVisitor {

void visit(SalesRow row);

void visit(RefundRow row);

}

// ===== 具体访问者:新增一种"操作",只加一个类,元素类一行不改 =====

publicclass JsonExporter implements ReportVisitor {

privatefinal StringBuilder sb = new StringBuilder("[");

@Overridepublicvoid visit(SalesRow r)  { sb.append("{\"type\":\"sale\",\"region\":\"").append(r.region()).append("\"}"); }

@Overridepublicvoid visit(RefundRow r) { sb.append("{\"type\":\"refund\",\"reason\":\"").append(r.reason()).append("\"}"); }

public String toJson() { return sb.append("]").toString(); }

}

publicclass XmlExporter implements ReportVisitor { /* 同理,输出 XML */ }

// ===== 使用:同一批数据,换个访问者就换一种输出 =====

List<ReportElement> rows = List.of(

new SalesRow("华东", new BigDecimal("120000")),

new RefundRow("华南", new BigDecimal("-3000"), "七天无理由"));

JsonExporter json = new JsonExporter();

rows.forEach(r -> r.accept(json));        // 客户端只调 accept,具体行为由元素类型 + 访问者共同决定

System.out.println(json.toJson());

典型使用场景

· 对复杂对象结构做多种导出/统计(财务报表导出 Excel/PDF/JSON)

· 编译器与静态分析:AST 上的类型检查、代码生成、代码美化都是不同的 Visitor

· 注解处理器(APT)遍历代码元素

· 文件系统遍历(Files.walkFileTree)

· 复杂保险/计费规则:不同类型的保单适用不同计算口径

JDK 与主流框架里的出处

· java.nio.file.FileVisitor / SimpleFileVisitor —— Files.walkFileTree(path, visitor)

· javax.lang.model.element.ElementVisitor + AbstractElementVisitor(编译期注解处理)

· ASM 字节码框架:ClassVisitor / MethodVisitor / AnnotationVisitor

· javax.lang.model.type.TypeVisitor、Hibernate/JPA 的元数据访问器

收益

· 新增操作非常容易:加一个访问者类即可(开闭原则)

· 把相关操作集中到一个类中,避免相关逻辑散落各处

· 可以跨越类层次结构累积状态(遍历时统计合计值)

代价与注意事项

· 新增元素类型极其痛苦:每加一种 Element,所有 Visitor 接口都要加一个 visit 方法 —— 所以只适用于"结构稳定"的场景

· 破坏了元素的封装(访问者往往需要读取元素内部字段)

· 双分派概念抽象,代码可读性较差,新人不易理解

和相似模式的区别

适用场景的硬约束:元素类型稳定 + 操作多变 → 用访问者;元素类型多变 → 千万别用,改用策略或在元素上直接加方法。现代替代方案(Java 17+):用 sealed interface + switch 模式匹配,可以在一个地方穷举所有元素类型,编译器还会检查是否遗漏分支 —— 可读性远好于双分派:

String toJson(Row r) { returnswitch (r) { case SalesRow s -> ...; case RefundRow f -> ...; }; }

(配合 sealed interface Row permits SalesRow, RefundRow {} 使用。)但注意:这种方式新增操作仍要改 switch(需改一处),而访问者新增操作是加一个类(零改动)。二者各有取舍。

易混淆模式对照表

· 简单工厂 vs 工厂方法:简单工厂把判断写在 if/switch 里,新增产品要改源码(违反开闭);工厂方法把判断下沉到子类工厂,新增产品只需新增类。简单工厂不是 GoF 23 种之一。

· 工厂方法 vs 抽象工厂:工厂方法造一个产品,靠继承扩展;抽象工厂造一族相互约束的产品,靠组合切换。维度从 1 变 N。

· 适配器 vs 桥接:适配器是事后补救(两个已有接口不兼容);桥接是事前设计(一开始就拆成抽象与实现两个维度,各自独立扩展)。

· 适配器 vs 装饰器 vs 代理:三者都"包一层",但意图不同:适配器换接口、装饰器加职责(可层层叠加、客户端知道原接口)、代理控访问(客户端以为在用原对象)。

· 策略 vs 状态:结构几乎一样,区别在意图:策略由客户端主动选择算法,彼此无感知;状态由对象内部条件自动切换,状态之间知道彼此并决定下一个状态。

· 策略 vs 命令:策略是"怎么做"(替换算法);命令是"做什么"(把请求封装成对象,可排队、可撤销、可记日志)。

· 观察者 vs 中介者:观察者是广播(多对多解耦,发布者不知订阅者细节);中介者是集中协调(把网状交互收敛到星型,中介者知道所有同事)。二者常组合使用:事件总线 = 观察者 + 中介者。

· 模板方法 vs 建造者:模板方法固定算法步骤,子类填其中几步;建造者固定装配流程,逐步构造一个复杂对象,客户端通常要拿到最终产品。

· 外观 vs 中介者:外观是单向简化(客户端 → 子系统),子系统不知道外观存在;中介者是双向协调,同事类都认识中介者并通过它通信。

· 享元 vs 单例:单例强调"全局唯一";享元强调"共享内部状态、分离外部状态",可以有多个不同的享元实例(如不同颜色的棋子)。

选型速查:我该用哪个?

· 对象创建逻辑复杂/参数爆炸:建造者、工厂方法

· 要控制实例数量或共享昂贵资源:单例、享元、原型

· 大量 if/else 按类型分支:策略、工厂方法 + Map 注册

· 大量 if/else 按状态分支:状态、责任链

· 需要一层层叠加功能:装饰器

· 想在方法前后统一加逻辑:代理(AOP)

· 老接口/第三方 SDK 不兼容:适配器、外观

· 多个类网状互相依赖:中介者

· 一个动作触发多处更新:观察者

· 多个处理器按序/按条件处理:责任链

· 需要撤销、排队、重试:命令、备忘录

· 流程骨架固定、细节各异:模板方法

· 树形结构需要统一处理:组合 + 访问者

· 两个维度独立变化(类要爆炸):桥接

常见误区

· 过度设计:只有 2 个实现却先上抽象工厂 + 策略 + 建造者,代码量翻三倍,收益为零。模式应该在第三次重复时重构出来,而不是第一次就写。

· 把模式的"结构"当成"目的":比如责任链不设终止条件导致请求丢失;装饰器层层嵌套到 8 层后没人知道最终行为是什么。

· 滥用单例:单例本质是带状态的全局变量,会让单元测试互相污染、隐藏依赖。现代 Java 项目更推荐由容器(Spring IoC)管理生命周期,而不是手写 getInstance()。

· 滥用继承:装饰器、桥接、策略的价值都来自"组合/委派",一旦改用继承就退化成类爆炸。

· 忽略并发:懒汉单例不加 volatile、享元池不加同步、观察者 list 用非线程安全集合,都是线上事故高发点。

相关学习资料