ARTICLE · 1267119
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 -> ...; }; }
易混淆模式对照表
· 简单工厂 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 用非线程安全集合,都是线上事故高发点。
















































































