03|工厂方法:让子类决定流程使用的产品
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.factorymethod。
系列导航:Java 25 设计模式学习指南
导出任务已经有一条固定流程:校验标题,然后使用格式化器生成结果。不同导出器只在产品选择上不同。如果每个子类复制整段 export,后续修改标题规则时就可能漏改一个分支。
我们希望父类拥有业务流程,同时让子类决定格式化器。注意变化点在“创建什么产品”,不是把所有业务步骤都开放给子类随意重写。
从问题提炼设计意图
工厂方法在创建者中声明产品创建方法,具体创建者通过覆写决定产品类型;父类流程依赖产品接口运行。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
Exporter |
抽象创建者,拥有 export 流程及 createFormatter 工厂方法 |
PlainExporter、MarkdownExporter |
具体创建者,覆写产品创建步骤 |
Formatter |
抽象产品 |
PlainFormatter、MarkdownFormatter |
具体产品 |
classDiagram
Exporter <|-- MarkdownExporter
Exporter <|-- PlainExporter
Formatter <|.. MarkdownFormatter
Exporter ..> Formatter : uses
MarkdownExporter ..> MarkdownFormatter : creates
代码思路:变化应该落在哪个对象上
export 是 final,先拒绝空白标题,再调用 createFormatter。这个调用发生在父类方法内部,但 Java 动态分派会选择实际子类的实现,因此父类能够使用自己并未直接构造的产品。
MarkdownExporter 只返回 MarkdownFormatter;产品再完成具体格式化。创建者和产品是两组不同角色,不要把它们合并理解成“换一种继承写法”。创建者负责组织任务,产品负责被任务调用的能力。
新增其他格式时,可以补一个产品实现和一个创建者子类,让公共校验保持不动。应用最外层仍需决定实例化哪个创建者,例如在启动装配处选择 MarkdownExporter。模式把变化移动到受控的扩展点,没有消除选择本身。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.factorymethod,源码目录为 src/main/java/com/hanserwei/patterns/factorymethod/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
Exporter.java:
package com.hanserwei.patterns.factorymethod;
/** 固定导出流程,把产品创建延迟给子类. */
public abstract class Exporter {
/** 校验标题后通过工厂方法获取产品并执行导出. */
public final String export(String title) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("Title must not be blank");
}
return createFormatter().format(title);
}
/** 由子类决定本次流程使用哪一种格式化产品. */
protected abstract Formatter createFormatter();
}
MarkdownExporter.java:
package com.hanserwei.patterns.factorymethod;
/** 选择具体产品的创建者. */
public final class MarkdownExporter extends Exporter {
/** 创建与本导出器匹配的格式化产品. */
@Override
protected Formatter createFormatter() {
return new MarkdownFormatter();
}
}
Formatter.java:
package com.hanserwei.patterns.factorymethod;
/** 导出流程需要的产品契约. */
public interface Formatter {
/** 格式化已经通过校验的非空标题. */
String format(String title);
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.factorymethod;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
Exporter exporter = new MarkdownExporter();
System.out.println(exporter.export("Java 25"));
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runFactorymethod
./gradlew test --tests 'com.hanserwei.patterns.factorymethod.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
# Java 25
用测试确认模式的行为
两个创建者复用校验但输出不同;空白标题在导出入口失败。测试能区分“只换产品”和“复制出两套流程”。
对应测试位于 src/test/java/com/hanserwei/patterns/factorymethod/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 框架提供固定业务流程,扩展者需要选择参与流程的产品。
- 解析、导出、连接创建等组件需要通过继承扩展。
- 父类需要复用校验与编排,同时保留产品创建时机。
适用边界与容易踩的坑
若父类除了 createFormatter 外没有任何复用流程,只剩“一类创建一类”的空壳,新增创建者层级的收益可能很低。这时简单工厂,或给服务构造器注入 Formatter,通常更直接。
工厂方法不一定每次 new;也可以返回已有对象,但必须解释共享对象的可变状态、资源归属和关闭责任。本例的无状态产品每次创建,避免让缓存问题遮住创建者与产品的关系。
子类必须遵守父类契约:返回可用的 Formatter,不能返回 null,也不应绕过业务不变量。final export 防止替换整个流程,并不能证明每个未来工厂方法实现都正确。
与相近模式比较
模板方法关注整个算法的可变步骤;工厂方法专注其中的产品创建步骤。一个模板流程调用工厂方法是正常组合。本章使用的 export 就带有模板流程特征,但学习焦点是 createFormatter 的动态分派。
动手练习
新增带前缀的产品与创建者,保持 Exporter 不变。然后尝试改为构造器注入 Formatter,比较两种方案的装配代码:什么情况下你确实需要创建者继承,什么情况下组合已经足够?
系列导航:Java 25 设计模式学习指南