模板方法模式
模板方法模式:Java 设计模式中的流程标准化艺术
在 Java 设计模式体系中,模板方法模式(Template Method Pattern)是一种经典的行为型模式,它的核心价值在于 “流程标准化与细节差异化分离”。通过定义一个固定的算法骨架,将算法中不确定的步骤延迟到子类中实现,既能保证整体流程的一致性,又能灵活应对不同场景下的细节变化。这种模式在框架设计、业务流程标准化等场景中应用广泛,如 Spring 的生命周期管理、IO 流的读取逻辑等。
一、模板方法模式的核心定义与设计思想
1. 官方定义
模板方法模式(Template Method Pattern):定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中,使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。
2. 设计本质:“骨架固定,细节可变”
模板方法模式的核心逻辑可概括为 “父类定规矩,子类填细节”:
-
父类(抽象模板):负责定义算法的整体流程(模板方法),同时声明抽象方法(钩子方法),这些抽象方法对应算法中需要子类自定义的步骤;
-
子类(具体模板):继承抽象模板,实现父类声明的抽象方法,完成算法中与自身场景相关的细节逻辑,但无法修改父类定义的整体流程。
这种设计思想完美解决了 “流程统一” 与 “细节灵活” 的矛盾 —— 例如,制作咖啡和茶的流程都包含 “煮水→冲泡→倒出→加调料” 四个步骤,其中 “煮水”“倒出” 是固定流程,而 “冲泡”(用咖啡粉还是茶叶)、“加调料”(加糖奶还是蜂蜜)是可变细节,模板方法模式就能很好地适配这种场景。
二、模板方法模式的结构与角色
模板方法模式通常包含 2 个核心角色,结构简洁且职责明确:
| 角色名称 | 职责描述 |
|---|---|
| 抽象模板(Abstract Template) | 1. 定义算法的骨架(模板方法),该方法通常用final修饰,防止子类修改流程;2. 声明抽象方法(或钩子方法),对应算法中需要子类实现的可变步骤;3. 可实现一些通用的固定步骤(具体方法),供模板方法直接调用。 |
| 具体模板(Concrete Template) | 1. 继承抽象模板,实现父类声明的抽象方法,完成与自身场景相关的细节逻辑;2. 可选择性重写父类的钩子方法(若父类提供默认实现),进一步定制流程。 |
结构示意图
抽象模板(AbstractClass)
├─ 模板方法(templateMethod(),final修饰):定义算法骨架
├─ 固定步骤(concreteMethod1()):父类实现,子类直接使用
├─ 抽象方法(abstractMethod1()):子类必须实现的可变步骤
└─ 钩子方法(hookMethod()):父类提供默认实现,子类可重写(可选)
↓ 继承
具体模板A(ConcreteClassA)
├─ 实现abstractMethod1():A场景的细节逻辑
└─ 重写hookMethod()(可选):A场景的定制化逻辑
↓ 继承
具体模板B(ConcreteClassB)
├─ 实现abstractMethod1():B场景的细节逻辑
└─ 不重写hookMethod():使用父类默认逻辑
三、模板方法模式实现(实例演示)
以 “制作饮品” 为例,咖啡和茶的制作流程存在共性(煮水、倒出)和差异(冲泡、加调料),通过模板方法模式实现流程标准化与细节差异化。
1. 步骤 1:定义抽象模板(AbstractDrink)
抽象模板类定义 “制作饮品” 的固定流程(模板方法),并声明可变步骤的抽象方法:
/**
* 抽象模板:饮品制作流程
*/
public abstract class AbstractDrink {
/**
* 模板方法:定义制作饮品的固定流程(final修饰,防止子类修改)
*/
public final void makeDrink() {
boilWater(); // 固定步骤1:煮水
brew(); // 可变步骤1:冲泡(子类实现)
pourInCup(); // 固定步骤2:倒出到杯子
addCondiments(); // 可变步骤2:加调料(子类实现)
}
/**
* 固定步骤:煮水(父类实现,所有饮品通用)
*/
private void boilWater() {
System.out.println("步骤1:煮水至100℃");
}
/**
* 固定步骤:倒出到杯子(父类实现,所有饮品通用)
*/
private void pourInCup() {
System.out.println("步骤3:将饮品倒入杯子");
}
/**
* 抽象方法:冲泡(子类必须实现,咖啡用咖啡粉,茶用茶叶)
*/
protected abstract void brew();
/**
* 抽象方法:加调料(子类必须实现,咖啡加糖奶,茶加蜂蜜)
*/
protected abstract void addCondiments();
}
2. 步骤 2:实现具体模板(咖啡、茶)
具体模板类继承抽象模板,实现抽象方法,完成自身场景的细节逻辑:
/**
* 具体模板1:制作咖啡
*/
public class Coffee extends AbstractDrink {
@Override
protected void brew() {
System.out.println("步骤2:用沸水冲泡咖啡粉");
}
@Override
protected void addCondiments() {
System.out.println("步骤4:加入糖和牛奶");
}
}
/**
* 具体模板2:制作茶
*/
public class Tea extends AbstractDrink {
@Override
protected void brew() {
System.out.println("步骤2:用沸水冲泡茶叶");
}
@Override
protected void addCondiments() {
System.out.println("步骤4:加入蜂蜜");
}
}
3. 步骤 3:客户端测试(调用模板方法)
客户端只需创建具体模板实例,调用父类定义的模板方法,即可触发完整流程,无需关注细节实现:
/**
* 客户端:测试饮品制作流程
*/
public class Client {
public static void main(String[] args) {
// 制作咖啡
AbstractDrink coffee = new Coffee();
System.out.println("=== 开始制作咖啡 ===");
coffee.makeDrink();
System.out.println("n=== 开始制作茶 ===");
// 制作茶
AbstractDrink tea = new Tea();
tea.makeDrink();
}
}
4. 运行结果(验证流程一致性)
=== 开始制作咖啡 ===
步骤1:煮水至100℃
步骤2:用沸水冲泡咖啡粉
步骤3:将饮品倒入杯子
步骤4:加入糖和牛奶
=== 开始制作茶 ===
步骤1:煮水至100℃
步骤2:用沸水冲泡茶叶
步骤3:将饮品倒入杯子
步骤4:加入蜂蜜
从结果可见,咖啡和茶的制作流程(步骤 1、3)完全一致,而差异化步骤(步骤 2、4)由子类分别实现,既保证了流程统一,又实现了细节灵活。
四、进阶:钩子方法的应用(增强灵活性)
在抽象模板中,除了抽象方法,还可定义 “钩子方法(Hook Method)”—— 父类提供默认实现,子类可根据需求选择是否重写,进一步增强流程的灵活性。例如,某些人制作咖啡时不需要加调料,可通过钩子方法控制 “加调料” 步骤是否执行。
1. 优化抽象模板(添加钩子方法)
public abstract class AbstractDrink {
// 模板方法(固定流程)
public final void makeDrink() {
boilWater();
brew();
pourInCup();
// 钩子方法:判断是否需要加调料
if (needCondiments()) {
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,子类可重写)
* @return true-加调料,false-不加调料
*/
protected boolean needCondiments() {
return true;
}
}
2. 子类重写钩子方法(无调料咖啡)
/**
* 具体模板3:无调料咖啡(重写钩子方法,取消加调料步骤)
*/
public class CoffeeWithoutCondiments extends AbstractDrink {
@Override
protected void brew() {
System.out.println("步骤2:用沸水冲泡黑咖啡粉");
}
@Override
protected void addCondiments() {
// 即使实现该方法,因钩子方法返回false,也不会执行
System.out.println("步骤4:加入糖和牛奶");
}
/**
* 重写钩子方法:不需要加调料
*/
@Override
protected boolean needCondiments() {
return false;
}
}
3. 客户端测试(验证钩子方法效果)
public class Client {
public static void main(String[] args) {
System.out.println("=== 开始制作无调料咖啡 ===");
AbstractDrink blackCoffee = new CoffeeWithoutCondiments();
blackCoffee.makeDrink();
}
}
4. 运行结果(流程按需调整)
=== 开始制作无调料咖啡 ===
步骤1:煮水至100℃
步骤2:用沸水冲泡黑咖啡粉
步骤3:将饮品倒入杯子
可见,通过重写钩子方法,子类成功取消了 “加调料” 步骤,在不修改父类流程骨架的前提下,实现了流程的定制化,这正是钩子方法的核心价值。
五、模板方法模式的优缺点分析
优点
-
流程标准化:父类统一定义算法骨架,避免子类重复编写相同流程代码,保证所有子类的流程一致性;
-
细节灵活化:子类可按需实现抽象方法或重写钩子方法,灵活应对不同场景的细节差异,符合 “开闭原则”;
-
代码复用性高:父类实现通用固定步骤,子类专注于差异化细节,减少代码冗余;
-
易于维护:若需修改整体流程,只需调整父类的模板方法(无需修改子类);若需修改细节,只需调整对应子类(无需影响其他子类)。
缺点
-
子类数量可能增多:若差异化场景较多,会导致具体模板类数量增加,增加系统复杂度;
-
继承局限性:模板方法模式基于继承实现,子类无法同时继承多个抽象模板(Java 单继承特性),限制了灵活性;
-
流程修改风险:若父类模板方法逻辑复杂,修改时可能影响所有子类,需谨慎操作。
六、模板方法模式的适用场景
- 流程标准化场景:当多个业务场景的核心流程一致,仅细节步骤存在差异时,如:
-
不同支付方式(微信支付、支付宝支付)的流程:“创建订单→发起支付→回调通知→订单确认”(流程固定,支付接口调用细节不同);
-
不同报表导出(Excel、PDF)的流程:“查询数据→数据格式化→写入文件→下载文件”(流程固定,文件写入细节不同)。
- 框架底层设计场景:框架需为使用者提供标准化流程,同时允许使用者定制细节,如:
-
Spring 的
AbstractApplicationContext:定义容器初始化流程(模板方法),子类(如ClassPathXmlApplicationContext)实现配置文件读取等细节; -
Java IO 流的
InputStream:read()方法为抽象方法,子类(如FileInputStream、ByteArrayInputStream)实现不同数据源的读取细节,而close()等通用方法由父类或工具类实现。
- 避免代码重复场景:当多个类存在大量重复代码(尤其是流程性代码),可将重复流程提取到父类模板方法中,子类仅保留差异化代码。
七、总结与注意事项
核心总结
-
模板方法模式的核心是 “父类定骨架,子类填细节”,通过
final修饰的模板方法保证流程不可变,通过抽象方法和钩子方法实现细节灵活; -
钩子方法是增强灵活性的关键,可让子类按需调整流程步骤的执行(如 “是否执行”“执行顺序”),而非仅实现细节;
-
该模式完美平衡了 “流程统一” 与 “细节灵活”,是框架设计和业务流程标准化的重要工具。
注意事项
-
模板方法用 final 修饰:父类的模板方法必须用
final关键字修饰,防止子类修改流程骨架,否则会破坏模式的核心逻辑; -
抽象方法职责单一:父类声明的抽象方法应遵循 “单一职责原则”,每个抽象方法对应一个明确的细节步骤,避免一个方法包含多个无关逻辑;
-
钩子方法合理使用:钩子方法需提供默认实现(避免子类强制重写),仅在需要定制流程时才由子类重写,避免滥用导致流程混乱;
-
避免抽象模板过于复杂:若父类包含过多抽象方法和钩子方法,会导致抽象模板逻辑臃肿,可通过拆分抽象模板(按业务模块拆分)优化。
通过合理使用模板方法模式,可让系统的流程更规范、代码更简洁、维护更高效。在实际开发中,当遇到 “流程固定、细节可变” 的场景时,模板方法模式往往是最优选择之一。如果需要针对特定框架(如 Spring、MyBatis)或复杂业务场景(如工作流引擎)进一步分析模板方法的应用,可结合具体场景展开讨论。

浙公网安备 33010602011771号