第 14 章 · 设计模式:可维护代码的套路
本章目标:理解设计模式解决的是变化中的复用问题;掌握创建型(单例、工厂方法、建造者)、结构型(装饰器、适配器)、行为型(策略、模板方法、观察者、责任链)共 8 个企业高频模式;能把 ch06 的定价策略识别为策略 + 模板方法组合;为 toolkit-demo 实现报表导出管线综合运用多模式;对照 python-dev 中的鸭子类型实现。
学时建议:5~6 小时(含 2 小时跟练)
前置:完成 ch05 OOP、ch06 接口与抽象类、ch13 泛型。
14.1 场景说明:代码为什么会"腐化"?
toolkit-demo 的报表功能一开始只有一个 if (type == "csv") ... else if (type == "json") ...;三个月后加了 Excel、PDF,方法膨胀到 200 行,每改一处都可能碰坏其他格式。设计模式的价值不在"炫技",而在把会变的东西隔离到可以替换的单元里:
需求变化(加一种导出格式)
无模式:改大方法 → 风险扩散
有模式:新增一个类 → 旧代码不动(开闭原则)
| 原则 | 一句话 |
|---|---|
| 开闭原则 | 对扩展开放,对修改关闭 |
| 面向接口编程 | 依赖抽象而非具体类 |
| 组合优于继承 | 用"装配"替代"层级" |
说明:本章所有代码为教学虚构示例,练习目录仍为 ~/learn-java/toolkit-demo。
14.2 三大分类速览
| 分类 | 关注点 | 本章覆盖 |
|---|---|---|
| 创建型 | 对象怎么 new | 单例、工厂方法、建造者 |
| 结构型 | 类与对象怎么组合 | 装饰器、适配器 |
| 行为型 | 对象间怎么协作 | 策略、模板方法、观察者、责任链 |
GoF 共 23 种,本模块只精讲企业代码与 Spring 源码里真正高频的 8 种;其余(门面、代理、享元等)在 spring-boot-web / spring-cloud-web 结合框架再认。
14.3 单例:全局唯一实例
适用:配置中心、连接池、注册表。推荐枚举写法——天然防反射、防序列化破坏:
package demo.pattern;
/** 枚举单例 — Effective Java 推荐 */
public enum AppConfig {
INSTANCE;
private String env = "dev";
public String env() { return env; }
public void setEnv(String env) { this.env = env; }
}
AppConfig.INSTANCE.setEnv("staging");
System.out.println(AppConfig.INSTANCE.env()); // 任何地方拿到的都是同一实例
| 写法 | 问题 |
|---|---|
饿汉式(static final 字段) | 类加载即创建,浪费但安全 |
| 双重检查锁(DCL) | 必须配 volatile,写错就翻车 |
| 枚举 | 最简、最安全,推荐 |
Spring 的 Bean 默认就是单例作用域——框架帮你管,业务代码里很少需要手写单例。本节重在理解原理与坑。
14.4 工厂方法:把 new 的决定权交出去
导出器创建逻辑集中管理,调用方只面对接口:
package demo.pattern;
public interface Exporter {
String format();
byte[] export(String content);
}
class CsvExporter implements Exporter {
public String format() { return "csv"; }
public byte[] export(String content) {
return ("col_a,col_b\n" + content).getBytes(java.nio.charset.StandardCharsets.UTF_8);
}
}
class JsonExporter implements Exporter {
public String format() { return "json"; }
public byte[] export(String content) {
return ("{\"rows\":\"" + content + "\"}").getBytes(java.nio.charset.StandardCharsets.UTF_8);
}
}
/** 工厂:按格式名创建对应导出器 */
public final class ExporterFactory {
private ExporterFactory() {}
public static Exporter create(String format) {
return switch (format.toLowerCase()) {
case "csv" -> new CsvExporter();
case "json" -> new JsonExporter();
default -> throw new IllegalArgumentException("未知格式: " + format);
};
}
}
新增 Excel 导出?只加一个类 + 工厂里加一个分支,调用方零改动。Spring 的 BeanFactory、日志的 LoggerFactory 都是此模式。
14.5 建造者:参数很多时的优雅构造
构造器参数超过 4 个就该考虑 Builder(ch11 的连接配置就是典型):
package demo.pattern;
public class ReportJob {
private final String name;
private final String cron;
private final int retry;
private final boolean async;
private ReportJob(Builder b) {
this.name = b.name;
this.cron = b.cron;
this.retry = b.retry;
this.async = b.async;
}
public static class Builder {
private String name = "unnamed";
private String cron = "0 0 * * *";
private int retry = 0;
private boolean async = false;
public Builder name(String name) { this.name = name; return this; }
public Builder cron(String cron) { this.cron = cron; return this; }
public Builder retry(int retry) { this.retry = retry; return this; }
public Builder async(boolean async) { this.async = async; return this; }
public ReportJob build() {
if (retry < 0) {
throw new IllegalArgumentException("retry 不能为负");
}
return new ReportJob(this);
}
}
@Override
public String toString() {
return "ReportJob{name='%s', cron='%s', retry=%d, async=%b}"
.formatted(name, cron, retry, async);
}
}
ReportJob job = new ReportJob.Builder()
.name("daily-report")
.cron("0 30 6 * * *")
.retry(3)
.async(true)
.build();
若只是数据传输,ch06 的record+ 紧凑构造器往往更轻;Builder 适合可选参数多 + 构造需校验的场景(StringBuilder、Lombok@Builder、Spring 的RestTemplateBuilder皆如此)。
14.6 策略模式:算法族可互换
ch06 的 PricingStrategy 就是策略模式——运行时按条件选择计价算法:
┌─ StandardPricing(原价)
Context ├─ MemberPricing(会员价)
└─ BulkPricing(批量价)
// ch06 已实现的接口,此处看"如何选择策略"
PricingStrategy strategy = switch (customerLevel) {
case "member" -> new MemberPricing(0.95);
case "bulk" -> new BulkPricing(10, 0.9);
default -> new StandardPricing();
};
double total = strategy.lineTotal(item, quantity);
识别特征:一组可互换的算法 + 同一个接口 + 运行时选择。JDK 里的 Comparator、Spring 的 ResourceLoader 都是策略。
14.7 模板方法:固定流程,开放步骤
ch06 的 AbstractBulkPricing 同样是模板方法——父类定流程骨架,子类填差异步骤:
priceFor()(父类 final)
├─ 取基价(固定)
└─ applyRate()(abstract,子类实现)← 变化的只有这一步
| 策略 vs 模板方法 | 区别 |
|---|---|
| 策略 | 整个算法可换(组合,接口) |
| 模板方法 | 流程固定,只换步骤(继承,抽象类) |
经验法则:整个替换用策略;骨架不变用模板方法。Spring 的 JdbcTemplate(固定获取连接/释放连接,SQL 由你填)是模板方法的经典。
14.8 观察者:一对多的事件通知
toolkit-demo 报表生成后通知多个订阅方(日志、邮件、Webhook):
package demo.pattern;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/** 事件监听器 */
public interface ReportListener {
void onReportReady(String reportName);
}
/** 事件源 */
public class ReportPublisher {
private final List<ReportListener> listeners = new CopyOnWriteArrayList<>();