配套代码: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 设计模式学习指南

上一篇:简单工厂 · 下一篇:抽象工厂