模板方法模式详解:定义程序骨架与框架设计
目录
- 1. 什么是模板方法模式
- 2. 为什么需要模板方法模式
- 3. 模板方法模式的结构
- 4. 实现示例
- 5. 钩子方法的使用
- 6. 最佳实践与注意事项
1. 什么是模板方法模式
模板方法模式是一种行为型设计模式,它在一个方法中定义一个算法的骨架,将某些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
这种模式在框架设计中被广泛使用,比如:
- Spring框架中的各种Template类
- JUnit测试框架
- Servlet的生命周期方法
2. 为什么需要模板方法模式
模板方法模式主要解决以下问题:
- 代码复用:将公共的算法骨架提取到父类中
- 扩展性:允许子类通过重写特定方法来改变算法的定步骤
- 控制反转:父类控制算法的整体流程,子类提供具体实现
3. 模板方法模式的结构
UML类图
核心角色
- AbstractClass(抽象类):
- 定义了一个模板方法,该方法包含算法的骨架
- 定义了算法各个步骤的抽象方法
- 可能包含钩子方法(hook methods)的默认实现
- ConcreteClass(具体类):
- 实现父类中的抽象方法
- 可以覆盖钩子方法
- 不能覆盖模板方法
4. 实现示例
让我们通过一个制作饮料的例子来理解模板方法模式:
// 抽象类:定义饮料制作模板
public abstract class BeverageTemplate {
// 模板方法,定义了制作饮料的算法骨架
public final void prepareBeverage() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
// 基本方法 - 具体方法
private void boilWater() {
System.out.println("将水煮沸");
}
// 基本方法 - 具体方法
private void pourInCup() {
System.out.println("倒入杯中");
}
// 基本方法 - 抽象方法,由子类实现
protected abstract void brew();
// 基本方法 - 抽象方法,由子类实现
protected abstract void addCondiments();
// 钩子方法,决定是否需要调料
protected boolean customerWantsCondiments() {
return true; // 默认返回true
}
}
// 具体类:咖啡
public class Coffee extends BeverageTemplate {
@Override
protected void brew() {
System.out.println("用沸水冲泡咖啡");
}
@Override
protected void addCondiments() {
System.out.println("加入糖和牛奶");
}
}
// 具体类:茶
public class Tea extends BeverageTemplate {
private boolean wantsLemon;
public Tea(boolean wantsLemon) {
this.wantsLemon = wantsLemon;
}
@Override
protected void brew() {
System.out.println("用沸水浸泡茶叶");
}
@Override
protected void addCondiments() {
System.out.println("加入柠檬");
}
@Override
protected boolean customerWantsCondiments() {
return wantsLemon;
}
}
// 使用示例
public class BeverageDemo {
public static void main(String[] args) {
System.out.println("制作咖啡:");
BeverageTemplate coffee = new Coffee();
coffee.prepareBeverage();
System.out.println("\n制作加柠檬的茶:");
BeverageTemplate teaWithLemon = new Tea(true);
teaWithLemon.prepareBeverage();
System.out.println("\n制作不加柠檬的茶:");
BeverageTemplate teaWithoutLemon = new Tea(false);
teaWithoutLemon.prepareBeverage();
}
}
运行结果:
制作咖啡:
将水煮沸
用沸水冲泡咖啡
倒入杯中
加入糖和牛奶
制作加柠檬的茶:
将水煮沸
用沸水浸泡茶叶
倒入杯中
加入柠檬
制作不加柠檬的茶:
将水煮沸
用沸水浸泡茶叶
倒入杯中
运行结果说明:
- 咖啡和茶都遵循相同的制作流程(模板方法定义的算法骨架)
- 它们各自实现了自己的冲泡(brew)和加调料(addCondiments)方法
- 茶类通过钩子方法(customerWantsCondiments)控制是否需要加入调料
- 整个流程由父类控制,子类只需要实现特定的步骤
5. 钩子方法的使用
钩子方法(Hook Methods)是模板方法模式中的一个重要概念,它让子类可以对父类的算法流程进行干预和补充。
5.1 钩子方法的类型和作用
- 条件型钩子方法:
- 返回布尔值,决定是否执行某个步骤
- 用于控制算法流程
- 例如前面例子中的
customerWantsCondiments()
- 空实现钩子方法:
- 提供默认的空实现
- 子类可以选择性覆盖
- 用于在算法中插入可选的处理步骤
- 默认实现钩子方法:
- 提供默认实现
- 子类可以选择是否覆盖
- 用于提供通用的处理逻辑
5.2 钩子方法示例
让我们通过一个文件处理的例子来详细说明钩子方法的使用:
// 抽象文件处理器
public abstract class FileProcessor {
// 模板方法
public final void processFile(String filePath) {
if (!validateFile(filePath)) { // 条件型钩子
return;
}
readFile(filePath);
beforeProcess(); // 空实现钩子
processContent();
if (needBackup()) { // 条件型钩子
backup();
}
afterProcess(); // 空实现钩子
cleanup(); // 默认实现钩子
}
// 条件型钩子方法:验证文件
protected boolean validateFile(String filePath) {
// 默认实现:检查文件是否存在
return new File(filePath).exists();
}
// 空实现钩子方法:处理前的准备工作
protected void beforeProcess() {
// 空实现,子类可以选择性覆盖
}
// 空实现钩子方法:处理后的收尾工作
protected void afterProcess() {
// 空实现,子类可以选择性覆盖
}
// 条件型钩子方法:是否需要备份
protected boolean needBackup() {
return false; // 默认不备份
}
// 默认实现钩子方法:清理工作
protected void cleanup() {
System.out.println("执行基本清理工作");
// 子类可以选择覆盖或通过super调用
}
// 抽象方法
protected abstract void readFile(String filePath);
protected abstract void processContent();
protected abstract void backup();
}
// 文本文件处理器
public class TextFileProcessor extends FileProcessor {
private String content;
private boolean isImportant;
public TextFileProcessor(boolean isImportant) {
this.isImportant = isImportant;
}
@Override
protected void readFile(String filePath) {
System.out.println("读取文本文件: " + filePath);
// 实际的文件读取代码...
}
@Override
protected void processContent() {
System.out.println("处理文本内容");
}
@Override
protected void backup() {
System.out.println("备份文本文件");
}
// 覆盖条件型钩子方法
@Override
protected boolean needBackup() {
return isImportant; // 根据文件重要性决定是否备份
}
// 覆盖空实现钩子方法
@Override
protected void beforeProcess() {
System.out.println("文本处理前的准备工作");
}
// 扩展默认实现钩子方法
@Override
protected void cleanup() {
super.cleanup(); // 调用父类的清理方法
System.out.println("执行文本文件特定的清理工作");
}
}
// 使用示例
public class FileProcessorDemo {
public static void main(String[] args) {
System.out.println("处理普通文本文件:");
FileProcessor normalProcessor = new TextFileProcessor(false);
normalProcessor.processFile("test.txt");
System.out.println("\n处理重要文本文件:");
FileProcessor importantProcessor = new TextFileProcessor(true);
importantProcessor.processFile("important.txt");
}
}
运行结果:
处理普通文本文件:
读取文本文件: test.txt
文本处理前的准备工作
处理文本内容
执行基本清理工作
执行文本文件特定的清理工作
处理重要文本文件:
读取文本文件: important.txt
文本处理前的准备工作
处理文本内容
备份文本文件
执行基本清理工作
执行文本文件特定的清理工作
5.3 钩子方法使用技巧
- 条件型钩子方法的使用:
- 用于控制算法流程的分支
- 返回布尔值,命名应该表达明确的判断含义
- 例如:
shouldProcess()
、isValid()
、canExecute()
- 空实现钩子方法的使用:
- 在算法关键节点提供扩展点
- 子类可以选择是否实现
- 通常用于前置/后置处理
- 例如:
beforeXXX()
、afterXXX()
、onXXX()
- 默认实现钩子方法的使用:
- 提供基础实现,允许子类扩展
- 子类可以选择完全覆盖或调用super
- 适用于有通用处理逻辑的场景
5.4 注意事项
- 命名规范:
- 条件型钩子方法使用 is、should、can 等前缀
- 空实现钩子方法使用 before、after、on 等前缀
- 名称应该清晰地表达方法的用途
- 文档说明:
- 清晰说明钩子方法的作用
- 说明默认行为
- 说明子类可以如何使用该钩子
- 设计建议:
- 钩子方法应该是protected的
- 不要在钩子方法中放置太多逻辑
- 钩子方法应该是可选的,不影响核心算法
6. 最佳实践与注意事项
- 封装变化:
- 将容易变化的步骤定义为抽象方法
- 将稳定的步骤实现在抽象类中
- 使用钩子方法处理可选步骤
- 遵循开闭原则:
- 模板方法应该是final的,防止子类改变算法骨架
- 通过添加新的子类来扩展功能
- 不修改已有的代码结构
- 命名规范:
- 模板方法应该清晰地表达其用途
- 抽象方法的命名应该反映其在算法中的作用
- 钩子方法通常以 should、will、do 等词开头
- 注意事项:
- 避免模板方法过于复杂
- 抽象方法的数量要适中
- 在文档中清晰说明每个抽象方法的职责
使用场景
模板方法模式适用于以下场景:
- 多个类有相似的算法,只是其中某些步骤不同
- 需要控制子类扩展的时候
- 一次性实现算法的不变部分,并将可变部分留给子类
- 防止代码重复
优点
- 提高代码复用性
- 遵循开闭原则
- 符合单一职责原则
- 父类控制,子类实现
缺点
- 每个不同的实现都需要一个子类
- 可能会导致类的数量增加
- 父类可能会变得过于庞大
总结
模板方法模式是一种简单但强大的设计模式,它通过把不变的行为搬移到超类,去除子类中的重复复代码来体现它的优势。它是基于继承的代码复用的基本技术。在框架设计中有着广泛的应用。使用时要注意权衡父类和子类之间的责任分配,以及钩子方法的合理使用。