原先所有开单请求都生成纸质收据 paper:200。新增电子收据后,金额必须大于零的校验不能因为新增渠道被绕过。把渠道字符串放进同一个创建方法是一种修复,但若不同开单流程分别负责自己的产品,谁能扩展创建决策而不改共同流程?

不先造工厂,先保住旧约束

labs/16 的 Before.issue 校验正金额后直接生成纸质文本。这没有缺陷,直到电子渠道出现。After 的基类用 final issue 固定调用顺序:检查金额 → 调用 createReceipt → 取得文本。Paper 与 Electronic 子类仅决定产品;测试确认新 email:200、旧 paper:200,并断言两种子类对 0 都抛异常。它们是教学中的 Creator 和 ConcreteCreator;产品是返回 text() 的 Receipt。本地字符串收据不表示真实打印或发邮件。

1
2
3
4
5
6
public final String issue(int cents) {
requirePositive(cents);
return createReceipt(cents).text();
}

protected abstract Receipt createReceipt(int cents);

继承让子类控制被固定流程使用的创建钩子,这才是此处 GoF Factory Method 的意图,不是因为方法名称有 factory。Alternative.of(kind, cents) 是静态方法,其中 switch 选择纸质或电子收据,同时属于常说的 Simple Factory 风格:调用者传入品种,单一方法选择具体产品。Java 的静态工厂方法只指“由静态方法返回实例”这一构造接口形式,并不自动表示 GoF Factory Method;它也可以根本没有分支或可扩展层次。不能把三者写成同义词。

路径 谁选择收据 0 分与未知品种
Before 单一方法内写死纸质 0 分拒绝;无电子入口
After Paper / Electronic 子类覆写钩子 基类先拒绝 0 分
Alternative 调用者传 kind,静态方法中 switch 0 分、未知品种都拒绝

若只有一个品种,一个 new 就够;若品种固定且在一个入口选择,Alternative 少两个 Creator 子类。只有当多个独立流程需要改变产品同时复用校验/执行骨架时,继承的变化边界才值得支付。After 的新子类若返回 null,基类仍会失败;示例未把插件质量或外部副作用封进类型系统。

flowchart LR
    Caller[调用方] --> Creator[After.issue]
    Paper[Paper 子类] -.继承.-> Creator
    Electronic[Electronic 子类] -.继承.-> Creator
    Creator --> Hook[createReceipt 钩子]
    Hook --> Product[Receipt]

验证与练习

在 examples/design-patterns/ 运行 ./mvnw -B -ntp -pl labs/16 -am test 及 ./mvnw -B -ntp verify,实际输入、环境、退出码和原始输出见 examples/design-patterns/evidence/16/RUN.md。不是用打印结果代替断言;同一旧合同、电子新增合同和边界均有测试。没有测试并发、磁盘输出或邮件交付。

  1. 新增短信收据产品 sms:200,先为旧纸质、电子与新增文本及 0 分拒绝补断言,再分别修改子类与静态工厂,记录谁需要修改共同 issue。
  2. 如果最终只有纸质一种品种,删去工厂钩子写回直接构造;运行旧合同,解释何时不应保留空的子类层次。

参考资料

上一节:15 阅读模式先看变化;下一节:17 两种产品如何成套。