概述
模板方法模式(Template Method Pattern)是GoF行为型设计模式中极为经典的一种,其核心定义是:定义一个操作中的算法骨架,而将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的前提下,重新定义该算法的某些特定步骤。 这一模式的根本意图在于通过抽象父类固化不变的高层流程,同时将可变的具体行为下放至子类实现,从而实现代码复用与行为扩展的完美平衡。
在日常开发中,我们时常面临这样的困境:多个业务类的处理流程高度相似,仅在某些环节存在细微差异。若直接在每个类中复制粘贴流程代码,不仅导致大量重复,更使得流程逻辑散落各处,难以统一维护与升级。模板方法模式精准地解决了这一痛点——它将公共算法流程提取到抽象父类的模板方法中,子类只需填充差异化步骤即可。同时,父类通过钩子方法提供了精细的控制扩展点,允许子类在不修改骨架的前提下影响算法行为。
本文将从最朴素的代码重复问题出发,逐步引出模板方法模式的定义、结构与演进过程,深入剖析其在JDK、Spring、MyBatis、Netty等主流框架中的源码级应用,并重点拓展至分布式环境下的任务调度、事务处理、消息消费等场景。最后,通过五大典型业务场景的独立Demo演示及十余道专家面试题的精讲,力求为读者构建一幅从原理到实践、从单体到分布式的完整知识图谱。
一、模式定义与结构
1.1 GoF标准定义
根据《设计模式:可复用面向对象软件的基础》一书,模板方法模式的意图如下:
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
中文释义:在一个方法中定义一个算法的骨架,而将一些步骤的实现延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些特定步骤。
1.2 UML类图(Mermaid classDiagram)
classDiagram
class AbstractClass {
<<abstract>>
+final templateMethod() void
#abstract primitiveOperation1() void
#abstract primitiveOperation2() void
#hookMethod() boolean
}
class ConcreteClassA {
+primitiveOperation1() void
+primitiveOperation2() void
}
class ConcreteClassB {
+primitiveOperation1() void
+primitiveOperation2() void
+hookMethod() boolean
}
AbstractClass <|-- ConcreteClassA
AbstractClass <|-- ConcreteClassB
note for AbstractClass "templateMethod()是final的,固定算法骨架。\nprimitiveOperationX为抽象方法,强制子类实现。\nhookMethod()提供默认实现,子类可选择重写以控制流程。"
1.3 类图详细文字说明
上图以Mermaid classDiagram精确描述了模板方法模式的静态结构。AbstractClass作为抽象父类,承载着模式的核心契约。其中的 templateMethod() 被声明为 final,这是模板方法模式的关键设计——它确保了算法骨架不可被子类篡改。该方法内部按照既定顺序调用了 primitiveOperation1()、primitiveOperation2() 以及可选的 hookMethod(),从而定义了一个固定不变的执行序列。
primitiveOperation1() 与 primitiveOperation2() 被声明为 abstract,它们是强制子类必须实现的“抽象步骤”。子类 ConcreteClassA 与 ConcreteClassB 必须提供这两个方法的具体实现,从而注入差异化的业务逻辑。这种设计将“不变”的流程控制权牢牢掌握在父类手中,而将“可变”的行为细节交由子类填充,实现了依赖倒置原则与好莱坞原则的完美结合。
hookMethod() 则是一种特殊的钩子方法,它并非抽象方法,父类通常提供默认的空实现或返回布尔值的判断实现。子类可以根据需要选择性地重写钩子方法,从而在关键节点影响模板方法的执行路径(例如控制条件分支是否执行、决定是否跳过某一步骤等)。这种反向控制机制让父类在固定骨架的同时保留了足够的灵活性,堪称模板方法模式的点睛之笔。对照类图,ConcreteClassB重写了 hookMethod(),而ConcreteClassA则使用了父类的默认实现,二者在运行时的行为将因此而产生差异。
二、代码演进与实现
2.1 不使用模式的原始代码:痛点分析
假设我们需要实现一个简单的数据备份系统,支持备份到本地文件和备份到云端对象存储。如果不使用设计模式,开发人员可能会写出如下代码:
// 本地文件备份
class LocalFileBackup {
public void backup() {
System.out.println("【本地备份】开始执行...");
// 1. 连接数据源 (模拟)
System.out.println("【本地备份】连接本地数据库...");
// 2. 读取数据 (模拟)
String data = "DB_Data_123";
System.out.println("【本地备份】读取到数据: " + data);
// 3. 压缩数据 (模拟)
String compressed = data + ".gz";
System.out.println("【本地备份】数据压缩完成: " + compressed);
// 4. 写入文件
System.out.println("【本地备份】写入本地文件 /backup/data.bak");
// 5. 清理资源
System.out.println("【本地备份】关闭数据库连接");
System.out.println("【本地备份】备份完成。");
}
}
// 云端对象存储备份
class CloudStorageBackup {
public void backup() {
System.out.println("【云端备份】开始执行...");
// 1. 连接数据源 (模拟)
System.out.println("【云端备份】连接本地数据库...");
// 2. 读取数据 (模拟)
String data = "DB_Data_456";
System.out.println("【云端备份】读取到数据: " + data);
// 3. 加密数据 (模拟)
String encrypted = "AES(" + data + ")";
System.out.println("【云端备份】数据加密完成: " + encrypted);
// 4. 上传对象存储
System.out.println("【云端备份】上传至 AWS S3 bucket");
// 5. 清理资源
System.out.println("【云端备份】关闭数据库连接");
System.out.println("【云端备份】备份完成。");
}
}
痛点分析:
- 代码重复严重:连接数据源、读取数据、清理资源的逻辑在两处完全一致,却被迫重复编写,违反了DRY(Don't Repeat Yourself)原则。
- 流程不一致风险:由于缺乏统一的骨架约束,新加入的开发人员在实现第三个备份方式(如磁带备份)时,可能会遗漏“清理资源”步骤,或错误地调换了步骤顺序,导致系统不稳定。
- 难以统一修改:若需要在所有备份操作前添加统一的“权限校验”步骤,我们必须逐个修改每一个备份类,维护成本高昂且极易遗漏。
- 违反开闭原则:对扩展开放,对修改关闭。上述代码为了增加新功能必须修改现有类,设计弹性极差。
2.2 经典模板方法模式重构
现在,我们应用模板方法模式将上述代码重构为优雅的面向对象设计。
// ==================== 抽象父类 ====================
abstract class AbstractBackup {
// 模板方法:定义算法骨架,声明为 final 防止子类篡改顺序
public final void backup() {
beforeBackup(); // 钩子方法:备份前置操作
connectDataSource(); // 公共步骤:连接数据源
String data = readData(); // 公共步骤:读取数据(可被子类重载,但本例中复用)
String processedData = processData(data); // 抽象步骤:子类实现差异化处理(压缩/加密)
writeData(processedData); // 抽象步骤:子类实现差异化写入
closeConnection(); // 公共步骤:清理资源
afterBackup(); // 钩子方法:备份后置操作
}
// 公共步骤(可在父类中提供默认实现,子类可直接复用)
private void connectDataSource() {
System.out.println("【抽象父类】连接数据源...");
}
// 公共步骤:读取数据,子类可选择性重写,此处提供默认实现
protected String readData() {
System.out.println("【抽象父类】执行默认数据读取逻辑");
return "Sensitive_Data_XYZ";
}
// 抽象步骤:子类必须实现具体的数据处理逻辑(压缩 vs 加密)
protected abstract String processData(String rawData);
// 抽象步骤:子类必须实现具体的数据写入逻辑
protected abstract void writeData(String data);
// 公共步骤:关闭连接,私有方法不暴露给子类,确保逻辑一致
private void closeConnection() {
System.out.println("【抽象父类】关闭数据源连接。");
}
// 钩子方法1:提供默认空实现,子类可选择覆盖以添加前置行为
protected void beforeBackup() {
// 默认为空,不做任何事
}
// 钩子方法2:提供默认空实现,子类可选择覆盖以添加后置行为
protected void afterBackup() {
// 默认为空
}
}
// ==================== 具体子类:本地文件备份 ====================
class LocalFileBackupRefactored extends AbstractBackup {
@Override
protected String processData(String rawData) {
String compressed = rawData + ".gz";
System.out.println("【本地备份子类】压缩数据: " + rawData + " -> " + compressed);
return compressed;
}
@Override
protected void writeData(String data) {
System.out.println("【本地备份子类】写入本地文件系统: /backup/" + System.currentTimeMillis() + ".bak");
}
// 重写钩子方法,添加本地备份特有的前置校验
@Override
protected void beforeBackup() {
System.out.println("【本地备份子类】检查本地磁盘空间是否充足...");
}
}
// ==================== 具体子类:云端对象存储备份 ====================
class CloudStorageBackupRefactored extends AbstractBackup {
@Override
protected String processData(String rawData) {
String encrypted = "AES256(" + rawData + ")";
System.out.println("【云端备份子类】加密数据: " + rawData + " -> " + encrypted);
return encrypted;
}
@Override
protected void writeData(String data) {
System.out.println("【云端备份子类】通过HTTPS上传至云端存储桶: my-bucket/" + System.currentTimeMillis());
}
// 不重写钩子方法,直接使用父类空实现
}
// ==================== 客户端测试 ====================
public class TemplateMethodDemo {
public static void main(String[] args) {
System.out.println("====== 执行本地文件备份 ======");
AbstractBackup localBackup = new LocalFileBackupRefactored();
localBackup.backup(); // 客户端只需调用模板方法,流程由父类控制
System.out.println("\n====== 执行云端存储备份 ======");
AbstractBackup cloudBackup = new CloudStorageBackupRefactored();
cloudBackup.backup();
}
}
重构成果分析:
- 消除重复代码:连接数据源与关闭资源的公共逻辑被提升至抽象父类中,并在模板方法
backup()中被统一调用。 - 流程强制统一:
backup()方法被声明为final,任何子类都无法修改步骤的执行顺序,保证了备份流程的原子性与一致性。 - 扩展点清晰:
processData()与writeData()作为抽象方法,明确界定了子类的职责边界。钩子方法beforeBackup()和afterBackup()则为非强制性的扩展预留了空间。 - 符合开闭原则:新增一种备份方式(如磁带备份)时,只需继承
AbstractBackup并实现两个抽象方法即可,无需触碰已有代码。
2.3 模板方法模式的进阶特性
a. 钩子方法的多种形态
钩子方法是模板方法模式的灵魂所在,其常见形态包括:
- 控制流程分支的钩子:子类通过返回布尔值决定是否执行某个步骤。
- 提供空实现的可选步骤钩子:如前例中的
beforeBackup(),子类可根据需要覆盖。 - 带返回值的判断钩子:影响父类算法逻辑走向。
以下为示例代码片段:
abstract class AdvancedDataProcessor {
public final void process() {
initialize();
if (isValid()) { // 钩子方法决定是否继续执行核心逻辑
doProcess();
}
cleanup();
}
protected abstract void doProcess();
protected void initialize() { /* 默认空实现 */ }
protected void cleanup() { /* 默认空实现 */ }
// 钩子方法:子类可覆盖以控制流程分支
protected boolean isValid() {
return true; // 默认有效
}
}
b. 好莱坞原则:“不要调用我们,我们会调用你”
模板方法模式是好莱坞原则(Hollywood Principle)的典型实现。在框架设计中,高层组件(抽象父类)定义了整体流程与调用时机,而低层组件(子类)无需主动调用高层方法,只需被动地等待高层在合适的时机回调自己。
示例解读:在 AbstractBackup 中,子类 LocalFileBackupRefactored 从不主动调用 backup() 方法中的任何步骤,它仅仅实现了 processData() 和 writeData()。当客户端调用 backup() 时,父类主导了整个流程,并在适当的时机回调子类的具体实现。这种反向控制极大地降低了耦合度,使得高层模块可以自由调整流程而不影响低层模块。
c. 与策略模式的对比
| 对比维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 复用方式 | 基于继承,子类复用父类骨架 | 基于组合,上下文类持有策略接口引用 |
| 关注点 | 固定算法骨架,局部步骤变化 | 整体算法的完全替换 |
| 关系类型 | 编译时确定(继承层次) | 运行时动态切换(接口注入) |
| 控制反转 | 好莱坞原则(父调子) | 委托调用(上下文调策略) |
| 代码粒度 | 细粒度步骤控制 | 粗粒度策略替换 |
结论:若一个算法的主干流程已经固定且未来极少变更,仅部分环节需要定制化,则优先选用模板方法模式;若整个算法族都需要频繁替换或动态组合,则策略模式是更佳选择。
2.4 模板方法模式执行流程图(Mermaid flowchart)
flowchart TD
A["客户端调用 templateMethod"] --> B["父类 final templateMethod 开始执行"]
B --> C["执行 beforeBackup 钩子方法"]
C --> D["执行 connectDataSource 公共步骤"]
D --> E["执行 readData 公共步骤"]
E --> F{"调用 processData 抽象方法"}
F -->|"压缩逻辑"| G["子类A 实现压缩逻辑"]
F -->|"加密逻辑"| H["子类B 实现加密逻辑"]
G --> I{"调用 writeData 抽象方法"}
H --> I
I -->|"写入本地文件"| J["子类A 写入本地文件"]
I -->|"上传云端"| K["子类B 上传云端"]
J --> L["执行 closeConnection 公共步骤"]
K --> L
L --> M["执行 afterBackup 钩子方法"]
M --> N["模板方法结束,返回客户端"]
流程图详细说明:
上述Mermaid flowchart清晰地呈现了模板方法模式的动态执行过程。当客户端调用抽象父类的 templateMethod(即 backup())时,控制权完全掌握在父类手中。流程首先执行公共前置钩子 beforeBackup,紧接着执行父类定义的公共步骤 connectDataSource 与 readData。在到达算法骨架中的变化点时,流程发生“分支”:processData 作为抽象方法,会根据运行时子类的实际类型回调相应的实现(子类A执行压缩,子类B执行加密)。同理,writeData 的调用也会根据子类类型分流至本地文件写入或云端上传逻辑。
值得注意的是,无论子类实现如何变化,父类都牢牢控制着 closeConnection 与后置钩子 afterBackup 的执行时机。这种设计确保了资源释放等关键步骤绝不会被子类遗漏,从而避免了内存泄漏或连接池耗尽等严重问题。整个流程如同一条装配流水线,父类定义了传送带的节奏与工位顺序,子类只需在指定工位上完成各自的装配动作即可。此图与前述UML类图形成了动与静、宏观与微观的互补关系。
三、源码级应用分析
3.1 JDK中的模板方法模式
1. java.io.InputStream
InputStream 是所有字节输入流的抽象基类,其 read() 方法族是模板方法模式的典范。
- 模板方法:
public int read(byte b[], int off, int len)定义了读取字节数组的通用流程,包括参数校验、循环读取直至满足要求等逻辑。该方法内部最终依赖于一个抽象方法public abstract int read()。 - 抽象步骤:
read()方法被声明为抽象,强迫子类实现“如何读取下一个字节”。 - 具体子类:
FileInputStream实现从文件系统读取字节;ByteArrayInputStream实现从内存字节数组读取;PipedInputStream实现从管道读取。每个子类仅需实现read()方法,复杂的缓冲、偏移计算完全由父类模板方法read(byte b[], int off, int len)完成。
2. java.util.AbstractList
AbstractList 是 List 接口的骨架实现类。
- 模板方法:
public E get(int index)依赖于子类实现的抽象方法public abstract E get(int index)。此外,indexOf(Object o)等方法的实现也基于get(int)和size()的组合。 - 钩子方法:
removeRange(int fromIndex, int toIndex)提供了一个受保护的空实现钩子,子类(如ArrayList)重写该方法以提供高效的批量删除操作。
3. java.util.concurrent.AbstractQueuedSynchronizer (AQS)
AQS 是 Java 并发包的核心,它使用了纯正的模板方法模式构建同步框架。
- 模板方法:
acquire(int arg)、acquireShared(int arg)、release(int arg)等均为 final 方法,定义了获取锁/释放锁的骨架,包括尝试获取、入队等待、自旋、阻塞等复杂状态流转。 - 抽象步骤(需子类实现):
tryAcquire(int arg)、tryRelease(int arg)、tryAcquireShared(int arg)、tryReleaseShared(int arg)。子类(如ReentrantLock$Sync、Semaphore$Sync、CountDownLatch$Sync)只需实现这些“尝试”方法即可实现不同的同步语义。 - 设计精髓:AQS 通过模板方法将复杂的并发控制逻辑与具体的同步语义解耦,使得开发自定义同步器变得极为高效。
4. javax.servlet.http.HttpServlet
Servlet 规范中的 HttpServlet 是模板方法模式的经典应用。
- 模板方法:
protected void service(HttpServletRequest req, HttpServletResponse resp)内部会调用String method = req.getMethod()判断请求类型,然后通过一系列 if-else 或 switch-case 语句将请求分发给对应的钩子方法doGet()、doPost()、doPut()等。 - 钩子方法:
doGet()、doPost()等在HttpServlet中默认返回 HTTP 405 错误(方法不支持)。子类(用户自定义 Servlet)只需重写关心的 HTTP 方法对应的钩子即可。
3.2 Spring框架深度剖析
1. AbstractApplicationContext
AbstractApplicationContext 的 refresh() 方法是 Spring IoC 容器启动的核心,也是模板方法模式在框架级应用中的巅峰之作。
// 简化示意
public abstract class AbstractApplicationContext {
public final void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
prepareRefresh(); // 1. 准备刷新
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 2. 获取新鲜BeanFactory
prepareBeanFactory(beanFactory); // 3. 准备BeanFactory
try {
postProcessBeanFactory(beanFactory); // 4. 钩子:后置处理BeanFactory
invokeBeanFactoryPostProcessors(beanFactory); // 5. 调用BeanFactory后置处理器
registerBeanPostProcessors(beanFactory); // 6. 注册Bean后置处理器
initMessageSource(); // 7. 初始化消息资源
initApplicationEventMulticaster(); // 8. 初始化事件广播器
onRefresh(); // 9. 钩子:子类特殊刷新逻辑
registerListeners(); // 10. 注册监听器
finishBeanFactoryInitialization(beanFactory); // 11. 实例化所有单例
finishRefresh(); // 12. 完成刷新
} catch (BeansException ex) {
// 销毁...
}
}
}
// 钩子方法
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {}
protected void onRefresh() throws BeansException {}
}
refresh() 被声明为 final,确保了每个 Spring 容器的启动流程严格一致,杜绝了子类破坏启动顺序的可能性。而 postProcessBeanFactory() 与 onRefresh() 作为受保护的钩子方法,为子类(如 GenericWebApplicationContext、AbstractRefreshableWebApplicationContext)提供了定制化扩展点。
2. JdbcTemplate
JdbcTemplate 融合了模板方法模式与回调模式。
- 模板方法:
public <T> T execute(StatementCallback<T> action)定义了获取连接、创建 Statement、执行 SQL、处理异常、释放资源的固定流程。 - 变化部分:SQL 执行逻辑通过回调接口
StatementCallback<T>传入,用户无需关心资源管理。
3. TransactionTemplate
类似 JdbcTemplate,TransactionTemplate 的 execute(TransactionCallback<T> action) 方法定义了开启事务、执行业务逻辑、提交/回滚事务的骨架,业务逻辑由回调提供。
3.3 MyBatis框架
1. BaseExecutor
MyBatis 的执行器模块是模板方法模式的典型实现。
- 抽象父类:
BaseExecutor实现了Executor接口的大部分通用逻辑。 - 模板方法:
query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)定义了查询骨架:查询一级缓存 → 若未命中则调用queryFromDatabase()。 - 抽象步骤:
doQuery()、doUpdate()、doFlushStatements()等抽象方法。 - 具体子类:
SimpleExecutor(简单执行器)、ReuseExecutor(可重用预处理语句执行器)、BatchExecutor(批处理执行器)。它们分别实现doQuery()方法以完成特定场景的数据库交互。
2. BaseTypeHandler
MyBatis 的类型处理器同样遵循模板方法模式。BaseTypeHandler 定义了设置参数与获取结果的通用骨架,子类(如 IntegerTypeHandler、StringTypeHandler)仅需实现类型转换的原子步骤。
3.4 Netty框架
Netty 中的 ChannelHandler 体系大量运用了模板方法模式的变体——适配器模式结合钩子方法。
- 抽象父类(适配器):
ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter为接口的所有方法提供了空实现或默认传播实现。 - 钩子方法:用户继承适配器后,仅需重写自己感兴趣的事件方法(如
channelRead()、exceptionCaught()),父类的默认实现保证了未重写的方法不会破坏事件传播链。这实质上也是一种“固定骨架(事件传播机制)、钩子扩展(事件处理逻辑)”的模板方法思想。
四、分布式环境下的模板方法模式
随着微服务架构与云原生技术的普及,模板方法模式在分布式系统中同样展现出强大的生命力。它将复杂的分布式交互流程抽象为稳定的骨架,极大地简化了业务开发人员的认知负担。
4.1 分布式任务调度模板:XXL-Job / ElasticJob
在 XXL-Job 中,开发者需实现 IJobHandler 抽象类并重写 execute() 方法。调度中心负责触发执行,而具体的任务初始化、日志记录、执行结果上报、资源销毁等流程均由框架内部的模板方法 JobThread.run() 统一控制。ElasticJob 的 AbstractElasticJobExecutor 同样定义了作业执行的固定流程(如获取分片上下文、执行作业、处理异常等),用户只需实现 process() 方法。
4.2 分布式事务处理模板:Seata AT模式
Seata 的 AT 模式通过 GlobalTransactionalInterceptor 拦截器实现模板方法模式。其伪代码骨架如下:
// 模板方法(简化的拦截器逻辑)
public Object invoke(MethodInvocation invocation) {
return transactionalTemplate.execute(transactionInfo -> {
// 1. 开启全局事务(向TC注册)
beginGlobalTransaction();
try {
// 2. 执行业务逻辑
Object result = invocation.proceed();
// 3. 提交全局事务
commitGlobalTransaction();
return result;
} catch (Exception e) {
// 4. 回滚全局事务
rollbackGlobalTransaction();
throw e;
}
});
}
业务代码只需添加 @GlobalTransactional 注解,Seata 框架便通过模板方法模式接管了分支事务注册、全局提交、全局回滚等复杂的分布式协调步骤。
4.3 消息消费模板:RocketMQ
RocketMQ 的 DefaultMQPushConsumer 允许用户注册 MessageListenerConcurrently 或 MessageListenerOrderly。消费流程骨架(拉取消息、重平衡、提交位点、流量控制)由客户端内部闭环,仅将解析后的消息列表通过回调钩子 consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) 暴露给业务方。
4.4 微服务调用链模板:Spring Cloud Sleuth
TraceTemplate 或 Tracer 接口定义了创建 Span、注入上下文、报告 Span 的标准流程。开发者可以通过实现自定义的 SpanReporter 或继承 AbstractSpanHandler 来扩展追踪数据的输出方式,而无需关心 Span 生命周期管理的底层细节。
4.5 分布式锁模板:Redisson
Redisson 提供了 RLock 接口及 RedissonLock 实现,其内部使用了类似模板方法的模式。lock.lock() 方法内部处理了 Redis 连接检查、Lua 脚本执行、WatchDog 自动续期、解锁等步骤。我们可以进一步封装一个通用的分布式锁执行模板:
public interface DistributedLockTemplate {
<T> T execute(String lockKey, long waitTime, long leaseTime, Callback<T> callback);
}
4.6 分布式任务执行框架示例
以下提供一个完整的基于模板方法模式的分布式任务执行框架简化实现,演示如何将锁机制与任务生命周期结合。
// ==================== 抽象基类:分布式任务 ====================
abstract class BaseDistributedTask {
// 模板方法:定义分布式执行骨架,声明为 final
public final void execute(String taskId) {
String lockKey = "task:lock:" + this.getClass().getSimpleName() + ":" + taskId;
boolean lockAcquired = false;
try {
// 1. 获取分布式锁(钩子控制锁策略)
lockAcquired = acquireLock(lockKey);
if (!lockAcquired) {
System.out.println("获取锁失败,任务可能正在执行或已被其他节点抢占。");
return;
}
System.out.println("成功获取分布式锁: " + lockKey);
// 2. 前置处理(钩子)
preExecute(taskId);
// 3. 核心业务逻辑(抽象步骤,子类实现)
doExecute(taskId);
// 4. 记录执行成功日志
logSuccess(taskId);
} catch (Exception e) {
// 5. 异常处理(钩子)
handleException(taskId, e);
} finally {
// 6. 释放锁
if (lockAcquired) {
releaseLock(lockKey);
System.out.println("释放分布式锁: " + lockKey);
}
// 7. 后置清理(钩子)
postExecute(taskId);
}
}
// 抽象步骤:执行具体任务逻辑
protected abstract void doExecute(String taskId) throws Exception;
// 钩子方法:获取锁,默认基于 Redis 实现,子类可覆盖
protected boolean acquireLock(String lockKey) {
// 模拟 Redis SETNX 操作
System.out.println("【父类默认】尝试获取 Redis 锁: " + lockKey);
return true; // 模拟成功
}
// 钩子方法:释放锁
protected void releaseLock(String lockKey) {
System.out.println("【父类默认】释放 Redis 锁: " + lockKey);
}
// 钩子方法:前置操作(默认空实现)
protected void preExecute(String taskId) {}
// 钩子方法:后置操作(默认空实现)
protected void postExecute(String taskId) {}
// 钩子方法:成功日志记录
protected void logSuccess(String taskId) {
System.out.println("【框架日志】任务执行成功: " + taskId);
}
// 钩子方法:异常处理
protected void handleException(String taskId, Exception e) {
System.err.println("【框架异常处理】任务执行失败: " + taskId + ", 异常信息: " + e.getMessage());
}
}
// ==================== 具体子类:数据同步任务 ====================
class DataSyncTask extends BaseDistributedTask {
@Override
protected void doExecute(String taskId) throws Exception {
System.out.println("执行数据同步逻辑,任务ID: " + taskId);
// 模拟业务耗时
Thread.sleep(1000);
}
// 重写钩子:使用更严格的锁机制(如 Redisson 公平锁)
@Override
protected boolean acquireLock(String lockKey) {
System.out.println("【子类重写】使用 Redisson 公平锁获取: " + lockKey);
return true; // 模拟成功
}
@Override
protected void preExecute(String taskId) {
System.out.println("【子类钩子】同步前检查数据源连接...");
}
}
// ==================== 客户端测试 ====================
public class DistributedTaskDemo {
public static void main(String[] args) {
BaseDistributedTask task = new DataSyncTask();
task.execute("SYNC_20231024");
}
}
4.7 分布式任务模板执行架构图(Mermaid flowchart)
flowchart TD
subgraph 调度器集群
Scheduler[调度器节点]
end
subgraph 工作节点
Template[BaseDistributedTask.execute 模板方法]
Lock{获取分布式锁}
Pre[preExecute 钩子]
Core[doExecute 抽象方法]
Log[logSuccess 钩子]
Error[handleException 钩子]
Unlock[releaseLock 钩子]
Post[postExecute 钩子]
end
Scheduler -- 触发任务 --> Template
Template --> Lock
Lock -- 成功 --> Pre
Lock -- 失败 --> End1[结束: 锁竞争失败]
Pre --> Core
Core -- 业务执行 --> DataSync[DataSyncTask 具体实现]
DataSync --> Log
Log --> Unlock
Core -- 抛出异常 --> Error
Error --> Unlock
Unlock --> Post
Post --> End2[任务结束]
架构图详细说明:
上图描绘了一个分布式任务调度场景下的模板方法执行架构。左侧的“调度器集群”负责触发任务,通常由 Quartz、XXL-Job 或 K8s CronJob 担任。当任务被分发到某个工作节点时,控制流进入 BaseDistributedTask 的 execute 模板方法。流程的第一步是竞争分布式锁(acquireLock 钩子),这是分布式环境下保证任务幂等性、避免重复执行的关键设计。若获取锁失败,任务直接退出,防止资源浪费。
若成功获取锁,则依次执行 preExecute 钩子(如建立数据库连接)、核心抽象方法 doExecute(由子类 DataSyncTask 实现具体同步逻辑)、以及 logSuccess 钩子记录成功状态。整个过程被 try-catch-finally 结构严密包裹,任何异常都会被 handleException 钩子捕获并进行告警或补偿处理。最后,在 finally 块中强制调用 releaseLock 释放分布式锁,并执行 postExecute 进行资源清理(如关闭连接)。此架构通过模板方法模式将复杂的分布式协调逻辑(锁、异常、日志)与易变的业务逻辑彻底解耦,业务开发者只需关注 doExecute 中的核心数据处理,极大地提升了分布式任务开发的健壮性与效率。
五、对比辨析
5.1 模板方法模式 vs 策略模式
| 维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 复用机制 | 继承(白盒复用) | 组合(黑盒复用) |
| 算法控制权 | 父类控制骨架,子类填充步骤 | 客户端选择并替换整个策略对象 |
| 变化粒度 | 细粒度步骤变化 | 粗粒度算法整体替换 |
| 关系绑定 | 编译时静态绑定 | 运行时动态绑定 |
| 设计原则 | 好莱坞原则(不要找我们,我们找你) | 单一职责原则、开闭原则 |
| 典型场景 | 框架骨架搭建(Spring Refresh, AQS) | 算法族切换(支付方式、压缩算法) |
5.2 模板方法模式 vs 工厂方法模式
工厂方法模式往往是模板方法模式中的一个步骤。例如在 AbstractApplicationContext.refresh() 中,obtainFreshBeanFactory() 就是一个工厂方法,用于创建 BeanFactory 实例。模板方法关注“流程控制”,而工厂方法关注“对象创建的延迟”。二者常协同出现,但并不等同。
5.3 模板方法模式 vs 建造者模式
- 模板方法模式:关注算法步骤的执行顺序,强调的是行为流程的固化。
- 建造者模式:关注产品的构建步骤,强调的是对象创建的过程分离。
- 结合使用:建造者模式的
Director在指导建造顺序时,其construct()方法本身就可以看作是模板方法。
5.4 模板方法模式 vs 钩子与回调
- 钩子方法:是类内部定义的方法(通常是 protected),子类通过继承重写来干预父类行为。属于类级别的扩展。
- 回调接口:是外部传入的函数式接口或对象,父类通过调用该接口方法实现控制反转。属于对象级别的扩展(如
JdbcTemplate中的StatementCallback)。
5.5 好莱坞原则与依赖倒置
好莱坞原则(Don't call us, we'll call you)是模板方法模式的核心思想,它体现了控制反转。依赖倒置原则(DIP)强调高层模块不应依赖低层模块,二者都应依赖抽象。在模板方法模式中,父类(高层)定义了抽象步骤接口,子类(低层)实现该接口,父类依赖抽象而不依赖具体子类,完美符合依赖倒置原则。
六、适用场景分析(重点强化)
场景一:数据导入导出框架
Demo代码
// 抽象数据导出器
abstract class DataExporter {
// 模板方法
public final void export(String query) {
connect(); // 1. 连接数据源
List<Map<String, Object>> data = fetchData(query); // 2. 查询数据
String formatted = formatData(data); // 3. 格式化数据(抽象步骤)
if (includeHeader()) { // 钩子方法:是否包含表头
formatted = generateHeader() + formatted;
}
write(formatted); // 4. 写入目标(抽象步骤)
close(); // 5. 关闭连接
}
private void connect() { System.out.println("连接数据库..."); }
private List<Map<String, Object>> fetchData(String query) {
System.out.println("执行查询: " + query);
List<Map<String, Object>> mockData = new ArrayList<>();
Map<String, Object> row = new HashMap<>();
row.put("id", 1);
row.put("name", "Alice");
mockData.add(row);
return mockData;
}
private void close() { System.out.println("关闭数据库连接。"); }
// 抽象步骤:格式化
protected abstract String formatData(List<Map<String, Object>> data);
// 抽象步骤:写入
protected abstract void write(String content);
// 钩子方法:是否生成表头
protected boolean includeHeader() { return false; }
// 生成表头的具体逻辑可由子类覆盖
protected String generateHeader() { return ""; }
}
// CSV导出器
class CsvExporter extends DataExporter {
@Override
protected String formatData(List<Map<String, Object>> data) {
StringBuilder sb = new StringBuilder();
for (Map<String, Object> row : data) {
sb.append(row.get("id")).append(",").append(row.get("name")).append("\n");
}
return sb.toString();
}
@Override
protected void write(String content) {
System.out.println("写入CSV文件:\n" + content);
}
@Override
protected boolean includeHeader() { return true; }
@Override
protected String generateHeader() { return "ID,Name\n"; }
}
// Excel导出器
class ExcelExporter extends DataExporter {
@Override
protected String formatData(List<Map<String, Object>> data) {
return "[EXCEL_XML_FORMAT] " + data.toString();
}
@Override
protected void write(String content) {
System.out.println("生成Excel文件: " + content);
}
}
// 测试
public class ExporterDemo {
public static void main(String[] args) {
DataExporter csvExporter = new CsvExporter();
csvExporter.export("SELECT * FROM users");
DataExporter excelExporter = new ExcelExporter();
excelExporter.export("SELECT * FROM users");
}
}
Mermaid类图
classDiagram
class DataExporter {
<<abstract>>
+final export(query) void
-connect() void
-fetchData(query) List
#abstract formatData(data) String
#abstract write(content) void
-close() void
#includeHeader() boolean
#generateHeader() String
}
class CsvExporter {
+formatData(data) String
+write(content) void
+includeHeader() boolean
+generateHeader() String
}
class ExcelExporter {
+formatData(data) String
+write(content) void
}
DataExporter <|-- CsvExporter
DataExporter <|-- ExcelExporter
文字说明
在数据导入导出场景中,流程一致性是核心诉求。无论是导出为 CSV 还是 Excel,其宏观步骤总是“连接→查询→格式化→写入→关闭”。若任由各开发者自由发挥,极易出现忘记关闭连接、遗漏异常处理等问题。DataExporter 抽象类通过 final export() 方法固化了这一流程,将易错的资源管理步骤 connect() 和 close() 私有化隐藏,确保了连接的安全释放。
formatData 与 write 作为抽象方法,精准捕捉了导出格式的差异点。同时,includeHeader 钩子方法允许子类灵活控制是否生成表头,generateHeader 钩子则提供了定制表头内容的入口。这种设计不仅消除了重复代码,更重要的是建立了一套可执行的规范:未来若需增加 JSON 导出器,开发人员只需关注格式转换与写入,无需重新梳理数据库连接、资源关闭等底层逻辑,极大地降低了引入 Bug 的风险,也使得日志记录、性能监控等横切关注点可以轻松地植入模板方法中。
场景二:消息推送流程
Demo代码
// 抽象消息推送器
abstract class BaseMessagePusher {
public final PushResult push(Message msg) {
try {
// 1. 校验消息
if (!validateMessage(msg)) {
return PushResult.fail("消息校验失败");
}
// 2. 鉴权
if (!authenticate()) {
return PushResult.fail("鉴权失败");
}
// 3. 选择通道(抽象方法)
String channel = selectChannel(msg);
System.out.println("使用推送通道: " + channel);
// 4. 执行推送(抽象方法)
boolean success = doPush(channel, msg);
// 5. 是否重试(钩子方法)
if (!success && shouldRetry()) {
System.out.println("执行重试机制...");
success = doPush(channel, msg); // 简单重试一次
}
// 6. 记录结果
recordResult(msg, success);
return success ? PushResult.success() : PushResult.fail("推送失败");
} catch (Exception e) {
handleException(msg, e);
return PushResult.error(e.getMessage());
}
}
private boolean validateMessage(Message msg) {
return msg != null && msg.getContent() != null;
}
protected abstract boolean authenticate();
protected abstract String selectChannel(Message msg);
protected abstract boolean doPush(String channel, Message msg);
protected abstract void recordResult(Message msg, boolean success);
// 钩子方法:是否重试
protected boolean shouldRetry() { return true; }
// 钩子方法:异常处理
protected void handleException(Message msg, Exception e) {
System.err.println("推送异常: " + e.getMessage());
}
}
class Message {
private String content;
public Message(String content) { this.content = content; }
public String getContent() { return content; }
}
class PushResult {
private boolean success; private String msg;
private PushResult(boolean s, String m) { success = s; msg = m; }
public static PushResult success() { return new PushResult(true, "OK"); }
public static PushResult fail(String m) { return new PushResult(false, m); }
public static PushResult error(String m) { return new PushResult(false, m); }
}
// 极光推送实现
class JpushPusher extends BaseMessagePusher {
@Override protected boolean authenticate() { System.out.println("JPush 鉴权"); return true; }
@Override protected String selectChannel(Message msg) { return "jpush_channel"; }
@Override protected boolean doPush(String channel, Message msg) {
System.out.println("JPush 发送: " + msg.getContent());
return true;
}
@Override protected void recordResult(Message msg, boolean success) {
System.out.println("记录 JPush 推送结果");
}
}
// 测试
public class PushDemo {
public static void main(String[] args) {
BaseMessagePusher pusher = new JpushPusher();
pusher.push(new Message("Hello World"));
}
}
Mermaid流程图
flowchart TD
Start([开始推送]) --> Validate{校验消息}
Validate -- 失败 --> Fail1[返回校验失败]
Validate -- 通过 --> Auth{鉴权}
Auth -- 失败 --> Fail2[返回鉴权失败]
Auth -- 通过 --> Select[选择推送通道 抽象方法]
Select --> DoPush[执行推送 抽象方法]
DoPush --> CheckRetry{推送成功? 且 shouldRetry钩子}
CheckRetry -- 是: 重试 --> DoPush
CheckRetry -- 否 --> Record[记录推送结果 抽象方法]
Record --> Success[返回推送成功]
DoPush -- 异常 --> Handle[handleException钩子]
Handle --> Error[返回系统错误]
文字说明
消息推送是典型的高可用、多通道场景,业务逻辑极易因接入第三方SDK的差异而变得混乱。BaseMessagePusher 通过模板方法 push 抽象出了一条标准化的推送流水线:校验→鉴权→选通道→推送→重试→记录。这套流程对于极光推送、个推、Firebase 均是通用的。
在此模式加持下,监控埋点、异常告警等横切关注点可以统一在父类的 push 方法中处理,避免了每个通道实现类各自为政导致的监控盲区。例如,recordResult 方法的调用时机被父类严格锁定在推送完成之后,保证了运营数据报表的准确性。shouldRetry 钩子方法则提供了灵活的重试策略控制,某些对实时性要求极高的通道(如推送验证码)可以通过覆盖钩子关闭重试,防止用户收到重复短信。模板方法模式将流程稳定性与业务灵活性这对矛盾体完美调和,是构建健壮推送中台的不二之选。
场景三:自动化测试用例框架
Demo代码
abstract class BaseTestCase {
// 模板方法:定义测试生命周期
public final void runTest() {
setUp(); // 1. 准备环境
try {
if (needScreenshotOnStart()) { // 钩子:开始截图
takeScreenshot("Start");
}
test(); // 2. 执行测试(抽象步骤)
testSuccess = true;
} catch (AssertionError | Exception e) {
testSuccess = false;
onTestFailure(e); // 钩子:测试失败处理
} finally {
if (needScreenshotOnFinish()) { // 钩子:结束截图
takeScreenshot("Finish");
}
tearDown(); // 3. 清理环境
reportResult(); // 4. 生成报告(模板方法内定)
}
}
private boolean testSuccess = false;
protected abstract void setUp();
protected abstract void test() throws Exception;
protected abstract void tearDown();
// 钩子方法
protected boolean needScreenshotOnStart() { return false; }
protected boolean needScreenshotOnFinish() { return false; }
protected void onTestFailure(Throwable e) {
System.err.println("Test Failed: " + e.getMessage());
}
protected void takeScreenshot(String phase) {
System.out.println("截图保存: " + phase + ".png");
}
private void reportResult() {
System.out.println("测试结果: " + (testSuccess ? "通过" : "失败"));
}
}
// 具体测试用例:登录功能测试
class LoginTestCase extends BaseTestCase {
@Override protected void setUp() { System.out.println("启动浏览器,访问登录页"); }
@Override protected void test() throws Exception {
System.out.println("输入用户名密码,点击登录");
// 断言略
}
@Override protected void tearDown() { System.out.println("关闭浏览器"); }
@Override protected boolean needScreenshotOnFinish() { return true; }
}
// 测试运行器
public class TestRunner {
public static void main(String[] args) {
BaseTestCase loginTest = new LoginTestCase();
loginTest.runTest();
}
}
Mermaid时序图
sequenceDiagram
participant Runner as 测试运行器
participant Template as BaseTestCase
participant Sub as LoginTestCase
Runner->>Template: runTest()
Template->>Sub: setUp()
Sub-->>Template: 环境就绪
Template->>Template: needScreenshotOnStart()?
Template->>Sub: test()
Sub-->>Template: 执行测试逻辑
Template->>Template: needScreenshotOnFinish()?
Template->>Sub: tearDown()
Sub-->>Template: 资源清理
Template->>Template: reportResult()
Template-->>Runner: 测试完成
文字说明
自动化测试框架(如 JUnit、TestNG)的核心设计正是模板方法模式。JUnit 4 中的 @Before、@Test、@After 注解虽然通过反射机制调用,但其底层运行器 BlockJUnit4ClassRunner 执行的顺序逻辑本质上就是一个固定的模板骨架。我们自定义的 BaseTestCase 显式地还原了这一设计思想:runTest 作为 final 模板方法,严格定义了 setUp → test → tearDown 的生命周期。
这种设计的反向控制优势在于:测试框架(父类)掌控了执行流程与资源回收的绝对话语权。即便某个 test 方法抛出未捕获的异常,finally 块中的 tearDown 依然会坚定不移地执行,从而避免了因测试失败导致浏览器进程残留、数据库连接泄露等环境污染问题。钩子方法 needScreenshotOnFinish 和 onTestFailure 则为测试报告的可观测性提供了强大扩展点,开发者无需在每个测试用例中重复编写“如果失败则截图”的逻辑,只需继承基类并配置钩子即可。这体现了框架设计中的“约定大于配置”与“关注点分离”原则。
场景四:银行交易处理流程
Demo代码
abstract class AbstractTransactionProcessor {
protected Account sourceAccount;
protected Account targetAccount;
protected BigDecimal amount;
public AbstractTransactionProcessor(Account source, Account target, BigDecimal amt) {
this.sourceAccount = source;
this.targetAccount = target;
this.amount = amt;
}
// 模板方法
public final TransactionResult process() {
TransactionResult result = new TransactionResult();
try {
// 1. 验证账户状态
if (!validateAccounts()) {
return result.fail("账户状态异常");
}
// 2. 检查余额(钩子控制是否允许透支)
if (!checkBalance()) {
return result.fail("余额不足");
}
// 3. 执行扣款(抽象步骤)
boolean deducted = executeDeduction();
if (!deducted) {
return result.fail("扣款失败");
}
// 4. 记录流水
recordTransaction();
// 5. 发送通知
sendNotification();
result.success();
} catch (Exception e) {
handleException(e);
result.error(e.getMessage());
// 补偿机制:若扣款成功但后续失败,需冲正
compensate();
}
return result;
}
private boolean validateAccounts() {
return sourceAccount != null && sourceAccount.isActive();
}
private boolean checkBalance() {
if (isOverdraftAllowed()) { // 钩子方法:是否允许透支
return true;
}
return sourceAccount.getBalance().compareTo(amount) >= 0;
}
// 抽象步骤:执行具体扣款逻辑
protected abstract boolean executeDeduction();
private void recordTransaction() {
System.out.println("记录交易流水: 从 " + sourceAccount.getId() + " 转出 " + amount);
}
protected void sendNotification() {
System.out.println("发送交易短信通知...");
}
protected void handleException(Exception e) {
System.err.println("交易异常: " + e.getMessage());
}
// 补偿钩子:子类可重写以支持冲正
protected void compensate() {
System.out.println("执行默认补偿: 恢复余额");
sourceAccount.setBalance(sourceAccount.getBalance().add(amount));
}
// 钩子方法:是否允许透支
protected boolean isOverdraftAllowed() { return false; }
}
// 实体类
class Account {
private String id;
private BigDecimal balance;
private boolean active;
// 构造器及getter/setter省略...
public Account(String id, BigDecimal balance) {
this.id = id; this.balance = balance; this.active = true;
}
public String getId() { return id; }
public BigDecimal getBalance() { return balance; }
public void setBalance(BigDecimal balance) { this.balance = balance; }
public boolean isActive() { return active; }
}
class TransactionResult {
private boolean success; private String message;
public void success() { this.success = true; this.message = "交易成功"; }
public TransactionResult fail(String msg) { this.success = false; this.message = msg; return this; }
public void error(String msg) { this.success = false; this.message = msg; }
}
// 具体交易:转账
class TransferProcessor extends AbstractTransactionProcessor {
public TransferProcessor(Account source, Account target, BigDecimal amt) {
super(source, target, amt);
}
@Override
protected boolean executeDeduction() {
sourceAccount.setBalance(sourceAccount.getBalance().subtract(amount));
targetAccount.setBalance(targetAccount.getBalance().add(amount));
System.out.println("转账完成: " + amount);
return true;
}
}
Mermaid流程图
flowchart TD
Start([开始交易]) --> Validate[验证账户]
Validate -- 无效 --> EndFail1[返回失败]
Validate -- 有效 --> CheckBal{检查余额}
CheckBal -- 不足且不允许透支 --> EndFail2[返回余额不足]
CheckBal -- 通过 --> Deduct[执行扣款 抽象步骤]
Deduct -- 失败 --> Compensate[补偿/冲正]
Deduct -- 成功 --> Record[记录流水]
Record --> Notify[发送通知]
Notify --> Success[返回成功]
Deduct -- 异常 --> HandleEx[异常处理]
HandleEx --> Compensate
Compensate --> EndError[返回错误]
文字说明
金融交易系统对流程的原子性、一致性与审计合规性有着极致的要求。AbstractTransactionProcessor 通过模板方法模式将“验证→检查→扣款→记录→通知”这条黄金流程固化下来,任何子类(转账、取款、缴费)都不得逾越此雷池一步。这种强制性的流程约束是满足金融监管审计要求的基础保障。
尤为关键的是异常补偿机制的设计。在分布式或复杂业务逻辑中,executeDeduction 可能成功更新了数据库,但随后的 sendNotification 若因网络抖动失败,整个交易必须被视为失败并进行补偿(冲正)。模板方法在 catch 块中统一调用 compensate 钩子,为开发者提供了清晰的补偿入口。同时,isOverdraftAllowed 钩子允许特定账户(如 VIP 客户或内部对公账户)拥有透支权限,实现了规则与流程的解耦。这种模式确保了无论业务逻辑多么复杂多变,核心交易流程的严肃性与可靠性始终坚如磐石。
场景五:游戏回合制战斗流程
Demo代码
// 抽象战斗回合
abstract class AbstractBattleRound {
protected Character actor;
protected Character target;
public AbstractBattleRound(Character actor, Character target) {
this.actor = actor;
this.target = target;
}
// 模板方法
public final void executeRound() {
onRoundStart(); // 钩子:回合开始效果(如Buff触发)
Action action = selectAction(); // 抽象步骤:选择行动
System.out.println(actor.getName() + " 选择行动: " + action.getName());
int damage = calculateDamage(action); // 计算伤害
applyDamage(damage); // 应用伤害
if (target.getHp() <= 0) {
onTargetDefeated(); // 钩子:目标倒下
}
onRoundEnd(); // 钩子:回合结束效果
}
// 抽象步骤:选择行动(玩家输入 vs AI决策)
protected abstract Action selectAction();
// 公共计算逻辑
protected int calculateDamage(Action action) {
int base = action.getPower() + actor.getAttack();
// 可添加钩子影响伤害计算
base = modifyDamage(base);
return base;
}
private void applyDamage(int damage) {
System.out.println("对 " + target.getName() + " 造成 " + damage + " 点伤害");
target.setHp(target.getHp() - damage);
}
// 钩子方法群
protected void onRoundStart() {}
protected void onRoundEnd() {}
protected void onTargetDefeated() {
System.out.println(target.getName() + " 已倒下");
}
protected int modifyDamage(int damage) { return damage; } // 伤害修正钩子
}
// 角色类
class Character {
private String name; private int hp; private int attack;
// getter/setter...
public Character(String name, int hp, int attack) {
this.name = name; this.hp = hp; this.attack = attack;
}
public String getName() { return name; }
public int getHp() { return hp; }
public void setHp(int hp) { this.hp = hp; }
public int getAttack() { return attack; }
}
class Action {
private String name; private int power;
public Action(String n, int p) { name = n; power = p; }
public String getName() { return name; }
public int getPower() { return power; }
}
// 玩家控制回合
class PlayerRound extends AbstractBattleRound {
public PlayerRound(Character actor, Character target) { super(actor, target); }
@Override
protected Action selectAction() {
// 模拟玩家选择
return new Action("重击", 50);
}
}
// AI控制回合(具有Buff效果)
class AIRound extends AbstractBattleRound {
public AIRound(Character actor, Character target) { super(actor, target); }
@Override
protected Action selectAction() {
return new Action("撕咬", 40);
}
@Override
protected void onRoundStart() {
System.out.println("【AI Buff】 野性怒吼,攻击力提升!");
}
@Override
protected int modifyDamage(int damage) {
return (int)(damage * 1.2); // 增伤20%
}
}
Mermaid时序图
sequenceDiagram
participant Engine as 游戏引擎
participant Round as AbstractBattleRound
participant Sub as PlayerRound / AIRound
Engine->>Round: executeRound()
Round->>Sub: onRoundStart()
Sub-->>Round: 触发Buff
Round->>Sub: selectAction()
Sub-->>Round: 返回行动
Round->>Round: calculateDamage()
Round->>Sub: modifyDamage(钩子)
Sub-->>Round: 修正后伤害
Round->>Round: applyDamage()
alt 目标HP <= 0
Round->>Sub: onTargetDefeated()
end
Round->>Sub: onRoundEnd()
Sub-->>Round: 清理状态
Round-->>Engine: 回合结束
文字说明
游戏开发中,回合制战斗逻辑是典型的规则驱动流程。AbstractBattleRound 定义了“回合开始→选择行动→计算伤害→应用效果→回合结束”的不可变骨架。通过抽象方法 selectAction,我们将玩家输入(由UI层驱动)与 AI 决策(由行为树驱动)这两种截然不同的行为源统一到了同一套战斗规则之下。
钩子方法在此场景中展现了极高的扩展价值。onRoundStart 和 onRoundEnd 为 Buff/Debuff 系统提供了天然的挂载点——中毒效果可在回合开始时扣血,眩晕效果可在回合开始时跳过 selectAction。modifyDamage 钩子则允许技能、装备、元素克制等子系统在不侵入核心伤害公式的情况下动态调整最终伤害值。这种设计使得游戏战斗引擎(父类)能够保持数学模型的稳定与纯净,而将充满创意与变化的角色技能、特效逻辑隔离在子类的钩子实现中,完美践行了“开闭原则”,极大地降低了大型游戏项目后期维护“屎山代码”的风险。
七、面试题精选与专家级解答
1. 模板方法模式与策略模式的核心区别是什么?从设计原则角度分析何时选择哪种模式?
- 核心区别:模板方法基于继承,在父类中固化算法骨架,强调的是流程复用;策略模式基于组合,将算法整体抽象为接口,强调的是算法替换。
- 设计原则角度:模板方法符合好莱坞原则(IoC),父类调用子类;策略模式符合单一职责与接口隔离原则。
- 选择依据:若业务流程的主干已经非常成熟且极少变化,仅部分环节多变(如框架启动、Servlet处理),优先用模板方法;若整个算法族都需要动态切换,或者需要在运行时灵活组装(如支付方式、排序算法),优先用策略模式。深度思考:当模板方法的扩展点越来越多导致子类膨胀时,应考虑重构为策略模式,将变化部分提取为策略接口。
2. JDK中AbstractQueuedSynchronizer是如何使用模板方法模式的?请简述其设计原理。
AQS 定义了一个 final 的 acquire(int arg) 模板方法,内部流程为:tryAcquire 尝试快速获取 → 失败则 addWaiter 入队 → acquireQueued 自旋阻塞。子类同步器(如 ReentrantLock 的 Sync)必须重写 tryAcquire 方法来实现具体的加锁语义(如判断 state 是否为0、是否可重入)。AQS 负责处理极其复杂且容易出错的线程挂起、唤醒、队列维护逻辑,子类只需关注“是否获取成功”这一布尔判断,体现了关注点分离的极致设计。
3. Spring的AbstractApplicationContext中refresh()方法为什么被设计为final?它体现了什么设计思想?
refresh() 被声明为 final 是为了防止子类破坏 IoC 容器的标准启动顺序。Spring 容器启动包含十几个精密配合的步骤(如 invokeBeanFactoryPostProcessors 必须在 registerBeanPostProcessors 之前),顺序错乱将导致容器状态异常。这体现了模板方法模式的契约精神:父类定义了不可逾越的算法骨架。同时,postProcessBeanFactory 等受保护的钩子方法又为子类(如 GenericWebApplicationContext)提供了合法的扩展点,体现了对修改关闭,对扩展开放的开闭原则。
4. 什么是钩子方法?模板方法模式中钩子方法有哪些常见使用形式?请举例说明。
钩子方法是父类中声明并给出默认实现(通常为空或返回 true/false)的方法,子类可选择性地重写以干预模板方法的执行流程。常见形式:
- 控制流程分支:
if (isLogEnabled()) { log(); } - 空实现可选步骤:
beforeBackup()默认为空,子类可添加前置校验。 - 提供默认行为的钩子:
getRetryTimes()默认返回 3,子类可改为 5。 - 拦截器/过滤器钩子:如
preHandle返回布尔值决定流程是否继续。
5. 模板方法模式符合好莱坞原则吗?为什么?请用代码示例说明。
完全符合。好莱坞原则即“Don't call us, we'll call you”。在模板方法模式中,子类从不主动调用父类的流程控制方法(如 backup()),而是被动地等待父类在合适的时机调用子类重写的方法。例如在 AbstractBackup.backup() 中,父类在固定节点调用了 processData(),子类 LocalFileBackupRefactored 只是提供了该方法的实现,而无需知晓何时被调用。这种反向控制使得高层组件(父类)能够自由优化流程而不影响低层组件。
6. 如何在模板方法中实现步骤的异常处理与回滚机制?请给出设计思路。
在模板方法的 try-catch-finally 结构中嵌入补偿逻辑。例如在金融交易场景中:
public final void process() {
boolean deducted = false;
try {
deducted = executeDeduction();
recordTransaction();
} catch (Exception e) {
if (deducted) {
compensate(); // 钩子:执行冲正
}
throw e;
}
}
通过一个标志位记录关键步骤是否执行成功,结合 catch 块调用抽象的 compensate 钩子,将回滚策略的控制权交给子类,同时保证了模板流程的完整性。
7. MyBatis的BaseExecutor是如何通过模板方法模式统一查询流程并支持不同执行器类型的?
BaseExecutor.query() 实现了缓存查询的通用逻辑:先从 localCache 获取,若未命中则调用 queryFromDatabase()。queryFromDatabase() 内部又调用了抽象方法 doQuery()。SimpleExecutor、ReuseExecutor、BatchExecutor 通过实现 doQuery() 方法来执行具体的 JDBC 操作(如是否复用 Statement、是否批量提交)。BaseExecutor 封装了所有执行器共享的一级缓存逻辑和异常处理,子类仅需关注与数据库交互的细节。
8. 模板方法模式与建造者模式在控制流程方面有何异同?是否可以结合使用?
- 异:模板方法的
templateMethod执行的是行为序列,最终产生的是行为结果;建造者的Director.construct()执行的是构建步骤,最终产生的是复杂对象。 - 同:二者都强调步骤的顺序性,Director 本身就是一个模板方法的应用。
- 结合使用:可以。例如
AbstractApplicationContext.refresh()是模板方法,而其中创建BeanFactory的子步骤可能使用建造者模式来构建复杂的BeanDefinition。
9. 在分布式环境下,如何利用模板方法模式设计一个具备超时重试、熔断降级能力的RPC调用框架?
定义一个 AbstractRpcInvoker 抽象类:
public final Object invoke(RpcRequest req) {
for (int i = 0; i < getRetryTimes(); i++) {
try {
if (circuitBreaker.isOpen()) {
return fallback(req);
}
return doInvoke(req);
} catch (TimeoutException e) {
// 记录超时,继续重试
} catch (Exception e) {
circuitBreaker.recordFailure();
}
}
throw new RpcException("No available service");
}
protected abstract Object doInvoke(RpcRequest req);
protected int getRetryTimes() { return 3; }
子类只需实现 doInvoke(如 HTTP 调用或 Dubbo 调用),模板方法负责重试、熔断状态检查、降级回调 fallback 钩子。
10. 模板方法模式通过继承实现代码复用,是否违反了合成复用原则?如何权衡?
模板方法模式确实使用了继承(白盒复用),在一定程度上增加了父类与子类的耦合,这与合成复用原则(优先使用组合)表面上有冲突。权衡之道在于:当算法骨架本身是系统的核心资产且高度稳定时,使用模板方法模式带来的流程规范性与易维护性收益远大于继承带来的耦合代价。若发现子类需要重写大部分钩子甚至想要修改骨架,则说明该场景不适合模板方法,应果断重构为策略模式(组合)。在框架底层开发中,模板方法因其性能优势(无接口调用的虚方法开销)和语义清晰性被广泛接受。
八、总结
至此,本文从最基础的模式定义出发,历经代码重构演进、源码深潜、分布式架构适配、场景化实战演练直至面试难点解析,全方位、立体化地呈现了模板方法模式在 Java 技术栈中的专家级应用图景。掌握此模式,不仅能在日常编码中消除重复、规范流程,更能在阅读顶尖开源框架源码时豁然开朗,洞察架构师的设计哲学。希望本文能成为您技术精进之路上的有力助推。