← 返回 Java 后端知识路线
阶段 02集合与并发

设计模式与 Java LTS 更新:让抽象解决真实变化

用价格策略、装饰器和工厂组合一个小型结算器,并把 Java 21 基线与 JDK 25 新特性单独标记。

第 16 / 35 篇
策略模式装饰器工厂Java 21LTS

先看这一课值不值得学

学完后,你手里多了哪些代码积木

把反复出现的对象协作方式命名,并区分语言版本能力。

本课正式新增

语法 / API / 命令你必须会到什么程度
factory / strategy / observer用创建、替换和通知模式组织对象
record / sealed用 JDK 21 表达数据载体和封闭层次
switch 模式匹配按类型和结构选择分支

本课只借用,先别硬背

  • JDK 25 新能力单独标注,不进入 JDK 21 可运行基线

学完必须能独立写

  • 为通知和创建流程选择合适模式
  • 说明项目为什么保持 JDK 21 基线
本课目录
  1. 1. 现实问题:模式不是类名,而是变化的边界
  2. 2. 最小可运行示例:策略加装饰器
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:把模式放到可验证的边界
  6. 6. 易错排查
  7. 7. 一页复习

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 可以依赖 RendererNotifierAccessPolicy 接口,具体实现由 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 版本演进」的学习范围组织,正文与示例均为本站原创整理。