前言:在日常开发中,我们经常会遇到这样的场景------多个业务逻辑拥有完全相同的执行流程,仅在部分步骤的实现上存在差异。比如饮料冲泡(咖啡和茶的流程一致,仅冲泡和加调料步骤不同)、数据库操作(连接、执行SQL、关闭连接流程固定,SQL执行和结果映射不同)。如果每个逻辑都重复编写相同流程代码,不仅会造成冗余,还会增加后续维护成本。而模板方法模式,就是解决这类"流程固定、细节差异"问题的最优解之一。
本文将从「概念解析→核心结构→实战案例→框架应用→优缺点→注意事项」六个维度,手把手带你掌握模板方法模式,结合Java代码实战,新手也能轻松理解,建议收藏备用!
一、什么是模板方法模式?
模板方法模式(Template Method Pattern)是一种行为型设计模式 ,其核心思想是:定义一个算法的骨架,将算法中某些步骤的具体实现延迟到子类中,使得子类可以在不改变算法整体结构的前提下,重新定义算法中的特定步骤。
简单来说,就是"先定框架,再填细节"。就像我们写文章,先确定标题、目录、开头结尾的固定框架,再填充每个章节的具体内容------框架不变,内容可灵活调整。
官方定义(GoF):定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
二、模板方法模式的核心结构
模板方法模式主要包含两个核心角色,结构清晰,无需复杂的依赖关系,具体如下:
1. 抽象类(Abstract Class)
作为算法骨架的定义者,负责封装所有子类共有的固定流程,包含以下4种方法:
-
模板方法(Template Method):通常用final修饰,定义算法的执行顺序(骨架),调用其他方法完成整个流程,防止子类重写破坏流程结构。
-
抽象方法(Abstract Method):无具体实现,由子类强制实现,对应算法中"可变的细节步骤"。
-
具体方法(Concrete Method):有具体实现,封装子类共有的通用逻辑,无需子类重写。
-
钩子方法(Hook Method):有默认实现(通常为空或返回默认值),子类可选择性重写,用于干预模板方法的执行流程(比如控制某个步骤是否执行),是模板方法模式的灵活扩展点。
2. 具体子类(Concrete Class)
继承抽象类,主要负责两件事:
-
实现抽象类中所有的抽象方法,完成自身特有的细节逻辑;
-
可选重写钩子方法,根据需求调整模板方法的执行流程(非必须)。
3. UML类图(简化版)
为了更直观理解结构,附上简化版UML类图(清晰易懂,无需复杂绘制):
java
+-------------------+
| AbstractClass |
+-------------------+
| + final templateMethod() // 模板方法(骨架)
| + abstract method1() // 抽象方法(子类实现)
| + abstract method2() // 抽象方法(子类实现)
| + concreteMethod() // 具体方法(通用逻辑)
| + hookMethod() // 钩子方法(可选重写)
+-------------------+
▲
|
+-------------------+ +-------------------+
| ConcreteClassA | | ConcreteClassB |
+-------------------+ +-------------------+
| + method1() | | + method1() |
| + method2() | | + method2() |
| ± hookMethod() | | ± hookMethod() |
+-------------------+ +-------------------+
三、实战案例:饮料冲泡系统(经典案例)
我们以"咖啡和茶的冲泡流程"为例,实战实现模板方法模式。两者的冲泡流程完全一致,仅细节步骤不同,非常适合用模板方法模式解决。
冲泡流程(固定骨架):烧开水 → 冲泡 → 倒入杯子 → 加调料(可选)
差异点:冲泡的原料不同(咖啡粉/茶叶),加的调料不同(糖奶/柠檬);且部分用户可能不需要加调料(钩子方法控制)。
1. 定义抽象类(饮料模板)
java
// 抽象类:饮料冲泡模板(定义算法骨架)
abstract class Beverage {
// 模板方法:固定冲泡流程,final防止子类重写破坏结构
public final void prepareRecipe() {
boilWater(); // 通用步骤:烧开水
brew(); // 抽象方法:冲泡(子类实现)
pourInCup(); // 通用步骤:倒入杯子
if (customerWantsCondiments()) { // 钩子方法:控制是否加调料
addCondiments(); // 抽象方法:加调料(子类实现)
}
}
// 具体方法:通用逻辑,所有饮料都需要烧开水
private void boilWater() {
System.out.println("1. 烧开水(100℃)");
}
// 具体方法:通用逻辑,所有饮料都需要倒入杯子
private void pourInCup() {
System.out.println("3. 将饮料倒入杯子");
}
// 抽象方法:冲泡步骤(咖啡/茶实现不同)
protected abstract void brew();
// 抽象方法:加调料步骤(咖啡/茶实现不同)
protected abstract void addCondiments();
// 钩子方法:默认返回true(加调料),子类可重写
protected boolean customerWantsCondiments() {
return true;
}
}
2. 实现具体子类(咖啡、茶)
java
// 具体子类:咖啡
class Coffee extends Beverage {
// 实现抽象方法:冲泡咖啡
@Override
protected void brew() {
System.out.println("2. 用滤纸冲泡咖啡粉");
}
// 实现抽象方法:加咖啡调料
@Override
protected void addCondiments() {
System.out.println("4. 加入糖和牛奶");
}
// 可选重写钩子方法:模拟部分用户不加调料
@Override
protected boolean customerWantsCondiments() {
// 这里简化为固定返回false,实际可根据用户输入判断
return false;
}
}
// 具体子类:茶
class Tea extends Beverage {
// 实现抽象方法:冲泡茶叶
@Override
protected void brew() {
System.out.println("2. 用沸水浸泡茶叶");
}
// 实现抽象方法:加茶调料
@Override
protected void addCondiments() {
System.out.println("4. 加入柠檬片");
}
// 不重写钩子方法,使用默认实现(加调料)
}
3. 客户端测试
java
// 客户端代码
public class TemplateMethodTest {
public static void main(String[] args) {
System.out.println("=== 制作咖啡 ===");
Beverage coffee = new Coffee();
coffee.prepareRecipe();
System.out.println("\n=== 制作茶 ===");
Beverage tea = new Tea();
tea.prepareRecipe();
}
}
4. 运行结果
java
=== 制作咖啡 ===
1. 烧开水(100℃)
2. 用滤纸冲泡咖啡粉
3. 将饮料倒入杯子
=== 制作茶 ===
1. 烧开水(100℃)
2. 用沸水浸泡茶叶
3. 将饮料倒入杯子
4. 加入柠檬片
案例分析
从案例中可以看出:
-
抽象类Beverage定义了固定的冲泡流程(模板方法),将通用步骤(烧开水、倒杯子)封装为具体方法,避免重复代码;
-
咖啡和茶子类仅实现自身特有的步骤(冲泡、加调料),无需关心整体流程;
-
钩子方法customerWantsCondiments()实现了灵活控制------咖啡子类重写后不加调料,茶子类使用默认实现加调料,体现了模板方法的扩展性。
四、框架中的模板方法模式(实际应用)
模板方法模式在主流框架中应用广泛,以下是两个最常见的场景,帮你理解"实际开发中如何用":
1. Spring框架中的JdbcTemplate
Spring的JdbcTemplate是模板方法模式的工业级典范,它封装了JDBC操作的固定流程,简化了开发者的数据库操作。
固定流程(模板方法):获取数据库连接 → 创建Statement → 执行SQL → 处理结果集 → 关闭连接/释放资源;
可变细节(子类/回调实现):SQL语句、参数绑定、结果集映射(如RowMapper);
开发者只需关注"SQL编写"和"结果映射",无需处理繁琐的资源管理,这就是模板方法模式"封装不变、开放可变"的核心价值。
2. Java Servlet中的HttpServlet
HttpServlet类定义了HTTP请求处理的固定流程(模板方法service()):
service()方法会根据请求方式(GET/POST/PUT等),分发到对应的doGet()、doPost()等方法;
开发者编写Servlet时,无需重写service()方法,只需重写doGet()、doPost()等抽象方法(本质是钩子方法的变体),实现自身的业务逻辑。这正是模板方法模式的典型应用。
3. 其他常见场景
-
JUnit测试框架:setUp() → testXXX() → tearDown(),固定测试流程,用户只需实现testXXX()方法;
-
文档导出功能:固定流程(验证数据→加载数据→格式化→导出),不同格式(PDF/Excel/TXT)的格式化和导出步骤由子类实现;
-
游戏开发:固定流程(初始化→游戏循环→渲染→结束清理),不同游戏的渲染和逻辑实现由子类扩展。
五、模板方法模式的优缺点
任何设计模式都有适用场景,先明确其优缺点,才能在开发中合理使用。
优点
-
代码复用:将所有子类的通用逻辑提取到抽象类,减少重复代码,降低维护成本;
-
流程可控:抽象类固定算法骨架,子类无法修改流程,确保所有子类的执行逻辑一致性;
-
扩展灵活:子类只需实现抽象方法或重写钩子方法,即可扩展新的逻辑,符合"开闭原则";
-
简化开发:开发者只需关注可变细节,无需关心整体流程,提高开发效率。
缺点
-
继承强耦合:子类必须继承抽象类,受Java单继承限制,灵活性不足;
-
维护成本增加:如果抽象类的模板方法修改(比如增加步骤),所有子类都可能受到影响;
-
复杂度提升:如果抽象类中的步骤过多、钩子方法过多,会增加代码的理解和维护难度。
六、注意事项与使用场景
1. 适用场景
-
多个类拥有相同的执行流程,仅部分步骤实现不同;
-
需要固定算法骨架,确保所有子类遵循统一流程(如框架设计、标准化流程);
-
需要灵活扩展,允许子类自定义部分细节,且不破坏整体流程。
2. 注意事项
-
模板方法必须用final修饰,防止子类重写破坏流程结构;
-
抽象方法和钩子方法的访问修饰符建议用protected,确保子类可访问,同时避免外部直接调用;
-
钩子方法应提供默认实现,避免子类强制重写(除非必须);
-
避免在抽象类中添加过多细节,保持骨架的简洁性,否则会增加维护成本。
3. 与策略模式的区别(高频面试题)
很多开发者会混淆模板方法模式和策略模式,两者都解决"算法变化"问题,但核心思路不同,用表格清晰区分:
| 对比维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 实现方式 | 继承(子类扩展父类) | 组合(注入不同策略对象) |
| 流程控制 | 父类固定流程,子类修改细节 | 无固定流程,可动态替换整个算法 |
| 灵活性 | 结构固定,扩展受限 | 灵活度高,可运行时切换算法 |
| 适用场景 | 流程固定,细节差异 | 算法整体可变,需动态切换 |
七、总结
模板方法模式的核心价值,在于"分离不变与可变"------将不变的算法骨架封装在抽象类,将可变的细节延迟到子类,既保证了流程的一致性,又实现了灵活扩展。
它的使用门槛不高,核心是把握"模板方法(固定骨架)+ 抽象方法(可变细节)+ 钩子方法(灵活控制)"三个核心要素。在框架设计、标准化流程开发中,模板方法模式能极大提升代码复用率和可维护性。
最后记住:模板方法模式适合"流程固定、细节不同"的场景,如果需要动态切换整个算法,优先考虑策略模式;如果需要固定流程、灵活扩展细节,模板方法模式就是最优选择。