1. 现实问题:模式不是类名,而是变化的边界
结算可能有普通价、学生价和会员价,之后还会叠加优惠券、日志和审计。如果把条件写进一个巨大方法,每加一条规则就改动核心分支。策略模式把可替换算法抽成接口,装饰器在不改原对象的情况下叠加行为,工厂把具体实现的创建集中到边界。
但模式也可能被滥用:只有一个永远不变的实现,硬拆出五个接口只会增加跳转。选择模式前先写“未来哪一处会变化、谁需要替换、测试怎样隔离”,再决定抽象。
2. 最小可运行示例:策略加装饰器
import java.util.function.IntUnaryOperator;
public class PatternCheckoutDemo {
interface Discount { int apply(int cents); }
static final class Checkout {
private final Discount discount;
Checkout(Discount discount) { this.discount = discount; }
int total(int cents) { return discount.apply(cents); }
}
static Discount student() { return cents -> cents * 90 / 100; }
static Discount logged(Discount delegate) {
return cents -> {
int result = delegate.apply(cents);
System.out.println("结算=" + cents + " -> " + result);
return result;
};
}
public static void main(String[] args) {
Checkout checkout = new Checkout(logged(student()));
System.out.println(checkout.total(1_000));
}
}
这里的 lambda 是策略实现,logged 接受一个策略并返回包装后的策略,形成装饰器。Checkout 只依赖 Discount,不知道学生规则和日志细节。示例用整数分,避免把金额舍入问题藏在模式演示里。
3. 调用链与对象变化
工厂方法 student() 返回一个实现 Discount 的 lambda 对象;logged 保存它作为 delegate,创建另一个 lambda。Checkout 构造器保存外层引用,调用 total 时先进入日志装饰器,再调用内部策略,计算结果沿调用栈返回,最后输出副作用。
对象变化体现了组合:没有修改原 Discount,也没有创建继承层次;外层对象只持有内层对象。若再加审计、指标和限额,可以继续包裹,但要控制顺序,因为 limit(logged(discount)) 和 logged(limit(discount)) 的行为不同。
4. 为什么这样设计
策略解决“同一件事有多套算法”,装饰器解决“同一对象需要叠加横切行为”,工厂解决“调用者不应知道具体创建细节”。三者都服务于变化点,不能根据课程标题机械套用。模板方法适合稳定流程骨架,状态模式适合状态迁移规则,观察者适合事件通知,但每种模式都要问生命周期和失败处理。
Java 版本更新也应遵循同一原则:新语法能减少样板并保持团队可读性才引入。JDK 21 是本路线运行基线,记录模式、虚拟线程和模式匹配可以在明确运行时后使用;版本升级需同时看编译参数、容器镜像、CI 和监控。
5. 项目落点:把模式放到可验证的边界
博客系统的 Markdown 渲染、文章发布通知和权限策略都有真实替换点。Service 可以依赖 Renderer、Notifier、AccessPolicy 接口,具体实现由 Boot 配置注入;审计和指标优先用装饰器,不把日志复制到每个实现。每个策略有独立单测,组合顺序有集成测试。
练习:为结算增加满减策略和优惠券策略,要求返回值不可为负;用工厂按用户类型选择策略,再写一个 fake 记录调用次数。比较 if/else、策略 Map 和工厂组合的可读性,留下选择理由,不追求模式数量。
6. 易错排查
- 抽象没有变化点:删掉接口,用最小直接实现重新评估是否更清楚。
- 装饰器顺序错误:为每一层写输入、输出和副作用,测试组合顺序。
- 工厂变成巨大 switch:把创建配置外移,或按 key 注册策略 Map,避免新的分支回到核心。
- 升级 JDK 只改本机:同步检查编译器、CI、Docker 镜像和生产运行时。
7. 一页复习
模式描述对象如何协作,不是背类图。策略替换算法,装饰器叠加行为,工厂收口创建;接口依赖能力,组合保留变化。Java 21 是稳定基线,新特性必须有运行时和团队证据,不能因为语法更新就改写一套可用业务。
把结算需求画成选择表:算法会替换就选 Strategy,行为需要叠加就选 Decorator,具体实现由配置决定就用 Factory;如果只有一条固定流程和一个实现,直接方法反而更清楚。输入是 cents 和用户上下文,策略输出新金额,装饰器额外产生日志/指标副作用,工厂只负责创建,不应偷偷执行结算。
组合顺序要有可观察结果:先折扣再封顶和先封顶再折扣可能不同,日志装饰器放外层能记录最终金额,放内层只看到中间值。项目可将策略放在 domain/pricing,工厂放在 application 配置,日志和指标装饰器放在 infrastructure;测试分别验证单策略和组合时序,避免一个大测试只断言最终数字。
版本更新也要做输入输出实验。输入是 Java 21 编译参数、JDK 镜像和 CI 配置,输出应是同样的测试结果、启动日志和性能指标;如果只在本机安装 JDK 25,CI 和生产仍是 21,新增语法就不能进入主线。JDK 25 的能力要单独分支、单独标注和单独回滚,不能把基线文章写成“安装最新版就行”。
排错时结算金额错,先看策略顺序和整数舍入;新渠道没有生效,检查工厂 key、Bean 注册和配置 profile;切换 JDK 后编译通过但启动失败,检查依赖、反射和容器镜像;模式层级变多却没有替换测试,说明抽象没有真实变化点。练习是删掉一个接口重新写 if/else,再和策略版本比较文件数、测试隔离和下一次需求的改动范围。
设计模式练习可以记录对象状态而不只看最终金额:工厂输入用户类型得到某个策略对象,装饰器输入原始策略得到外层引用,调用时金额经过每一层并产生日志。若一个装饰器修改了订单状态,必须说明它是纯计算还是副作用;如果顺序可交换,要有测试证明,不能凭类名假设。
Java 版本治理应落到仓库文件:pom.xml/toolchain 固定 release,Dockerfile 固定 JRE,CI matrix 记录允许版本,README 标注 JDK 25 单独实验。升级前先编译、跑单测、跑启动/接口 smoke,再比较内存、启动和依赖告警;失败时保留上一镜像和上一 commit,而不是只在本机卸载新版。
排错时策略结果错查舍入和组合顺序,Bean 替换失败查扫描/Qualifier/配置,JDK 升级运行失败查反射和模块,模式过多难测试查是否存在真实替换输入。练习是为同一结算用例分别画 if/else、Strategy、Decorator 的对象图,写出选择某一种的收益和维护代价。
模式和版本练习都要留下反事实比较:如果不用 Strategy 会改哪些文件,如果继续 Java 21 会少什么风险。只有把收益、成本、测试和回滚说清,抽象或升级才值得进入项目。
验证清单:给结算器输入同一订单,分别选择折扣、封顶、日志装饰器,记录工厂返回的对象类型、每层收到和返回的金额、日志出现顺序;再删除一个策略 key,确认错误在配置/工厂边界而不是结算中途才出现。用 JDK 21 的 CI、Docker 和本机编译跑同一组测试,比较输出、启动日志和依赖解析;JDK 25 实验单独记录编译参数、运行镜像和回滚点。复盘时说明某个模式替换了哪一种真实变化,若只增加接口却没有替换输入,应该撤回抽象。
JDK 25 单独标注:版本更新如何进入工程决策
JDK 25 的新特性不作为本路线 JDK 21 示例的运行前提。若项目要采用 JDK 25,应先阅读对应发行版的官方迁移说明,建立独立分支,用 CI、依赖兼容性、启动时间和回滚方案验证,再决定是否把语法或运行时能力合入主线。课程文章只把它作为版本治理案例,不在基础代码里混用 25 专属 API。
本课按「Java 21 LTS、常见设计模式与 JDK 版本演进」的学习范围组织,正文与示例均为本站原创整理。