配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为 com.hanserwei.patterns.templatemethod

系列导航:Java 25 设计模式学习指南

上一篇:策略 · 下一篇:访问者

文章导出总要先校验和清理标题,再输出前缀、正文和后缀。Markdown 与带框文本的差异只在某些步骤。如果每个导出器自由实现 export,某个子类可能忘记校验,或把后缀拼在错误位置。

模板方法让父类固定整体算法,子类只提供有限步骤。本例保持继承关系简单,使“不能改什么”和“允许扩展什么”都能从 API 直接看出来。

从问题提炼设计意图

模板方法在父类定义算法骨架,把部分步骤延迟给子类实现或覆写,在保持流程结构的同时复用公共行为。

对象职责与协作关系

示例角色 职责
ArticleExporter 抽象模板,定义 final export 算法
formatBody 必须由子类实现的抽象步骤
header、footer 带默认实现的可选钩子
MarkdownExporter、FramedExporter 提供不同步骤行为的具体子类
classDiagram
    ArticleExporter <|-- MarkdownExporter
    ArticleExporter <|-- FramedExporter
    Demo ..> ArticleExporter : export

代码思路:变化应该落在哪个对象上

export 使用 final 防止子类替换流程:先拒绝空白标题,再 strip,最后按 header + formatBody + footer 顺序形成结果。模板入口拥有公共不变量,而非要求每个子类自觉复制校验。

formatBody 是抽象步骤,任何具体子类都必须给出实现。header 与 footer 默认返回空字符串,是可选钩子;MarkdownExporter 只需要实现正文,FramedExporter 则覆写三个步骤。

调用发生在父类方法内部,具体步骤依然会动态分派给实际子类。这种“父类流程调用子类扩展点”的控制关系,使框架能够控制时机,让扩展者只负责局部行为。

关键实现与独立运行

配套仓库中的包名是 com.hanserwei.patterns.templatemethod,源码目录为 src/main/java/com/hanserwei/patterns/templatemethod/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。

ArticleExporter.java

package com.hanserwei.patterns.templatemethod;

/** 固定校验、转换和包装顺序的导出模板. */
public abstract class ArticleExporter {
  /** 固定算法骨架,子类不能跳过标题校验或改变执行顺序. */
  public final String export(String title) {
    if (title == null || title.isBlank()) {
      throw new IllegalArgumentException("Title must not be blank");
    }
    return header() + formatBody(title.strip()) + footer();
  }

  /** 返回文档前缀;默认不添加前缀. */
  protected String header() {
    return "";
  }

  /** 由子类实现正文转换,接收已去除首尾空白的标题. */
  protected abstract String formatBody(String title);

  /** 返回文档后缀;默认不添加后缀. */
  protected String footer() {
    return "";
  }
}

FramedExporter.java

package com.hanserwei.patterns.templatemethod;

/** 覆盖三个步骤但不改变模板顺序的带边框导出器. */
public final class FramedExporter extends ArticleExporter {
  /** 添加左边框. */
  @Override
  protected String header() {
    return "[";
  }

  /** 保留校验和清理后的标题. */
  @Override
  protected String formatBody(String title) {
    return title;
  }

  /** 添加右边框. */
  @Override
  protected String footer() {
    return "]";
  }
}

Demo.java 展示调用方如何装配这些对象:

package com.hanserwei.patterns.templatemethod;

/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
  /** 禁止实例化演示入口. */
  private Demo() {}

  /** 运行独立示例;args 为未使用的命令行参数. */
  public static void main(String[] args) {
    ArticleExporter exporter = new FramedExporter();
    System.out.println(exporter.export(" Java 25 "));
  }
}

在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew

./gradlew runTemplatemethod
./gradlew test --tests 'com.hanserwei.patterns.templatemethod.PatternTest'

示例的业务输出如下,省略 Gradle 自身的任务提示:

[Java 25]

用测试确认模式的行为

两个子类都复用首尾空白清理;FramedExporter 的输出顺序为左框、正文、右框;两个子类都不能接受空白标题。

对应测试位于 src/test/java/com/hanserwei/patterns/templatemethod/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。

常见用法

  • 批处理、导入导出、测试生命周期等流程顺序稳定。
  • 框架定义何时执行动作,扩展者提供少量具体步骤。
  • 多个实现共享校验、准备和收尾,且步骤之间有稳定约束。

适用边界与容易踩的坑

不要在父类构造器里调用可覆写步骤,此时子类字段可能尚未初始化。把模板入口作为构造完成后显式调用的方法,更容易保证对象有效。

钩子越多,子类越难理解允许的组合。如果几乎每一步都要覆写,甚至需要改变执行顺序,说明父类骨架可能并不稳定,应考虑组合多个策略或独立流程对象。

涉及资源时,收尾不能只靠字符串式“先后拼接”。真实文件导出要用 try-with-resources 或 finally,明确失败路径上哪些步骤仍会执行。本例只处理字符串,不声称展示了资源管理模板。

与相近模式比较

工厂方法可以成为模板方法中的一个步骤,专门创建产品;策略则用组合替换算法,减少继承耦合。选择时先问流程顺序是否稳定,以及变化是一个步骤还是整体算法。

动手练习

增加导出审计钩子,明确成功与失败时是否调用。再实现一个 RecordingExporter 测试替身记录步骤调用顺序,验证异常发生后不会错误执行成功审计。

系列导航:Java 25 设计模式学习指南

上一篇:策略 · 下一篇:访问者