设计模式-模板方法模式详解
定义算法的骨架,让子类决定具体步骤
在软件构建中,往往一系列类拥有几乎相同的操作流程,但其中某些步骤的具体实现又各不相同。如果将这些流程在每个类中重复实现,会导致大量冗余代码;而如果强行抽象,又可能破坏各步骤的灵活性。
模板方法模式可以以一种优雅解决此矛盾。它如同一位建筑大师,先绘制出稳固的结构蓝图,再将具体的装修细节交给不同的施工队。
1. 模式核心:算法骨架与具体实现的分离
模板方法模式 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些特定步骤。
这个定义蕴含了两个关键角色:
- 模板方法 :一个定义在父类中的
final方法,它规定了算法不可更改的执行顺序。 - 基本方法 :算法中的各个步骤,通常是
abstract的,由子类去实现。
客户端 调用模板方法 执行步骤1: 子类实现 执行步骤2: 子类实现 执行钩子方法: 可选 返回结果
2. 场景带入:从数据处理的"流水线"说起
想象一个数据处理框架,它需要支持从不同来源(数据库、文件、API)读取数据,并进行处理和导出。整个流程是固定的:
- 打开数据源连接
- 读取原始数据
- 处理数据(清洗、转换)
- 关闭连接
- 导出结果
其中,第1、2、4步 的具体实现因数据源不同而差异巨大,而第3、5步则可能是可选的或存在默认实现。这正是模板方法模式的绝佳用武之地。
3. Java实现:一步步构建你的模板
让我们用代码将上述场景具象化。
步骤1:定义抽象模板类
此类是整个模式的基石,它声明了模板方法和一系列基本方法。
java
/**
* 抽象数据处理器 - 充当模板类
*/
public abstract class DataProcessor {
/**
* 模板方法 - 定义算法骨架,声明为final防止子类重写算法结构
*/
public final void process() {
openConnection();
String rawData = readData();
String processedData = processData(rawData); // 默认处理
closeConnection();
exportResult(processedData);
}
// 基本方法1:抽象方法,必须由子类实现
protected abstract void openConnection();
protected abstract String readData();
protected abstract void closeConnection();
// 基本方法2:具体方法,提供默认实现,子类可选择不覆盖
protected String processData(String rawData) {
System.out.println("执行默认数据清洗...");
return rawData.trim().toUpperCase(); // 示例:简单清洗
}
// 基本方法3:钩子方法,提供空实现,子类可选择性地覆盖
protected void exportResult(String data) {
// 默认不导出,子类可覆盖此方法以实现特定导出逻辑
}
}
步骤2:创建具体子类
子类负责实现算法骨架中的抽象步骤。
java
/**
* 数据库数据处理器
*/
public class DatabaseDataProcessor extends DataProcessor {
@Override
protected void openConnection() {
System.out.println("[数据库] 建立JDBC连接...");
}
@Override
protected String readData() {
System.out.println("[数据库] 执行SELECT查询,读取数据...");
return " raw_data_from_db ";
}
@Override
protected void closeConnection() {
System.out.println("[数据库] 关闭连接,释放资源。");
}
// 覆盖钩子方法,增加数据库特有的导出逻辑
@Override
protected void exportResult(String data) {
System.out.println("[数据库] 将处理结果写回从表: " + data);
}
}
/**
* 文件数据处理器
*/
public class FileDataProcessor extends DataProcessor {
@Override
protected void openConnection() {
System.out.println("[文件] 打开文件流...");
}
@Override
protected String readData() {
System.out.println("[文件] 从CSV文件读取数据...");
return " raw_data_from_csv ";
}
@Override
protected void closeConnection() {
System.out.println("[文件] 关闭文件流。");
}
// 覆盖处理数据的具体方法,提供文件特定的处理逻辑
@Override
protected String processData(String rawData) {
System.out.println("[文件] 执行特定格式解析和清洗...");
return rawData.trim().replaceAll(",", "|");
}
// 不覆盖exportResult,即使用默认的空实现(不导出)
}
步骤3:客户端调用
客户端无需关心具体的数据源类型,只需通过统一的模板接口进行操作。
java
public class Client {
public static void main(String[] args) {
System.out.println("=== 处理数据库数据 ===");
DataProcessor dbProcessor = new DatabaseDataProcessor();
dbProcessor.process(); // 调用统一的模板方法
System.out.println("\n=== 处理文件数据 ===");
DataProcessor fileProcessor = new FileDataProcessor();
fileProcessor.process();
}
}
输出结果:
=== 处理数据库数据 ===
[数据库] 建立JDBC连接...
[数据库] 执行SELECT查询,读取数据...
执行默认数据清洗...
[数据库] 关闭连接,释放资源。
[数据库] 将处理结果写回从表: RAW_DATA_FROM_DB
=== 处理文件数据 ===
[文件] 打开文件流...
[文件] 从CSV文件读取数据...
[文件] 执行特定格式解析和清洗...
[文件] 关闭文件流。
从输出可以清晰看到,算法骨架(连接-读取-处理-关闭-导出)是固定的,但每个步骤的具体行为由子类决定。
4. 模式解构:角色、关系与好莱坞原则
模板方法模式体现了经典的 "好莱坞原则":"别调用我们,我们会调用你。"
- 父类(高层组件)掌控着程序流程。
- 子类(低层组件)仅仅提供实现细节,在父类需要时被调用。
| 角色 | 对应类 | 职责 |
|---|---|---|
| 抽象类 | DataProcessor |
定义模板方法和一系列基本方法(抽象、具体、钩子)。 |
| 具体类 | DatabaseDataProcessor, FileDataProcessor |
实现抽象类中定义的抽象方法,以完成算法中与特定子类相关的步骤。 |
5. 深入辨析:模板方法 vs. 策略模式
两者都用于封装算法,但思想截然不同,是控制反转的两种体现。
| 维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 核心思想 | 定义算法骨架,部分步骤可变 | 定义算法家族,使其完全可互换 |
| 控制权 | 父类控制流程,子类填充细节(好莱坞原则) | 客户端决定使用哪种策略,控制权在客户端 |
| 代码复用 | 通过继承,在父类中复用公共流程代码 | 通过组合,将算法作为独立对象,复用算法本身 |
| 灵活性 | 改变算法结构需修改父类,改变步骤实现需新增子类 | 可在运行时灵活切换整个算法,符合开闭原则 |
| 关系 | 类层次结构(继承) | 对象组合关系 |
简单比喻:
- 模板方法:烹饪食谱。食谱规定了炒菜的固定步骤(热锅、下油、翻炒、调味),但"翻炒"的力度和"调味"的用料由厨师(子类)决定。
- 策略模式:出行策略。去机场可以选择打车、坐地铁或开车。这些是完全不同的、可互换的完整方案,由你(客户端)在出发前决定。
6. 模式优劣与最佳实践
优势
- 代码复用最大化:将公共行为搬移到父类,避免了代码重复。
- 反向控制:通过好莱坞原则,实现了依赖倒置,便于框架搭建。
- 良好的扩展性:增加新的子类即可扩展新的行为,符合"开闭原则"。
劣势
- 继承的固有限制:Java是单继承,一个类一旦继承了某个模板类,就无法再继承其他类。
- 可能导致子类泛滥:每个细微的差异都可能需要一个新的子类。
- 对里氏替换原则的挑战:如果子类对步骤的实现完全颠覆了父类的意图,可能会破坏程序逻辑。
最佳实践与应用场景
- Spring框架 :
JdbcTemplate,RestTemplate等是模板方法模式的典范。它们处理了资源获取、异常处理、事务控制等样板代码,用户只需通过回调(如RowMapper)提供SQL和结果映射逻辑。 - Servlet API :
HttpServlet的service()方法根据HTTP方法调用doGet(),doPost()等,子类只需重写这些具体方法。 - 适用于 :
- 多个类有相同算法,但部分步骤不同时。
- 需要控制子类扩展点,只允许子类重写特定方法时。
- 重构时,消除重复代码,将公共流程上移到父类。
模板方法模式通过一种巧妙的反向控制结构,在保持算法整体结构稳定的同时,赋予了具体步骤极大的灵活性。它是框架设计的基石,也是日常开发中提炼公共流程、消除重复代码的利器。