📕目录

class 卑微码农:
def __init__(self):
self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
self.发量 = 100 # 初始发量
self.咖啡因耐受度 = '极限'
def 修Bug(self, bug):
try:
# 试图用玄学解决问题
if bug.严重程度 == '离谱':
print("这一定是环境问题!")
else:
print("让我看看是谁又没写注释...哦,是我自己。")
except Exception as e:
# 如果try块都救不了,那就...
print("重启一下试试?")
self.发量 -= 1 # 每解决一个bug,头发-1
# 实例化一个我
我 = 卑微码农()
前言

在编程世界里,我们总在和"重复"与"变化"打交道。有些代码像厨房菜谱里的固定流程,比如"先备菜后烹饪";有些则像食材处理方式,青菜要焯水、肉类要腌制,各有不同。如果你在开发中遇到"流程固定但细节可变"的场景,却还在重复编写相似代码,那这篇关于模板方法模式的实战指南,可能会彻底改变你的编码习惯。
本文将从生活场景切入,用3个递进式C++案例,带你吃透模板方法模式的设计思想、实现细节、应用场景,以及和其他模式的核心区别。所有代码均可直接运行,兼顾新手入门与进阶提升。
一、从"番茄炒蛋"看透模板方法模式

在着急解释设计模式定义前,我们先走进厨房。不管是新手还是大厨,做番茄炒蛋的流程基本固定:
-
准备食材(番茄切块、鸡蛋打散)
-
炒制鸡蛋(热油、下锅、翻炒盛出)
-
炒制番茄(热油、下锅、加糖炒出汁水)
-
混合翻炒(鸡蛋回锅、加盐调味)
-
出锅装盘
但细节上每个人都有差异:有人喜欢番茄去皮,有人偏好溏心蛋,有人会加番茄酱提味。这里的"固定流程"就是算法骨架,"可变细节"就是需要子类实现的步骤——这正是模板方法模式的核心思想。
1.1 模式核心定义
模板方法模式(Template Method Pattern)是一种行为型设计模式,它定义一个算法的骨架,将某些步骤延迟到子类中实现。这样既能保证算法结构的一致性,又能让子类灵活定制具体步骤,实现"流程复用、细节差异化"。
用一句话总结:父类定规矩,子类填细节。这里的"规矩"就是模板方法,"细节"就是延迟到子类的抽象步骤。
1.2 核心角色拆解
模板方法模式仅需两个核心角色,结构非常简洁,这也是它能广泛应用的原因之一。
| 角色名称 | 核心职责 | C++实现要点 |
|---|---|---|
| 抽象类(Abstract Class) | 1. 定义算法骨架(模板方法);2. 声明抽象步骤(子类必须实现);3. 提供钩子方法(子类可选重写) | 模板方法用final修饰防止重写;抽象步骤用纯虚函数;钩子方法用虚函数并提供默认实现 |
| 具体子类(Concrete Class) | 1. 实现抽象类中的纯虚函数;2. 按需重写钩子方法 | 仅关注自身差异化逻辑,无需修改算法流程 |
关键设计原则:好莱坞原则("不要调用我们,我们会调用你")。父类模板方法主动调用子类实现的步骤,而非子类调用父类,彻底反转了依赖关系。
二、C++实战:从简单案例到工业级实现

理论讲完难免空洞,下面我们用3个由浅入深的C++案例,从基础实现到高级拓展,完整掌握模板方法模式的应用技巧。所有代码均经过编译验证,可直接复制到IDE中运行。
2.1 基础案例:实现不同口味的番茄炒蛋
需求:模拟厨房系统,番茄炒蛋的流程固定,但"食材预处理"和"调味"步骤因口味不同而变化(如原味、酸甜味、辣味)。
步骤1:定义抽象类(菜谱模板)
抽象类负责定义"番茄炒蛋"的固定流程,同时声明需要子类实现的差异化步骤。
#include <iostream>
#include <string>
using namespace std;
// 抽象类:番茄炒蛋菜谱模板
class TomatoEggRecipe {
public:
// 模板方法:定义算法骨架,用final防止子类重写,确保流程固定
void makeTomatoEgg() final {
prepareIngredients(); // 步骤1:准备食材(差异化)
fryEgg(); // 步骤2:炒鸡蛋(固定)
fryTomato(); // 步骤3:炒番茄(固定)
mixAndFry(); // 步骤4:混合翻炒(固定)
season(); // 步骤5:调味(差异化)
serve(); // 步骤6:出锅装盘(固定)
}
protected:
// 抽象方法:必须由子类实现的差异化步骤
virtual void prepareIngredients() = 0; // 食材预处理
virtual void season() = 0; // 调味方式
// 固定方法:所有子类共享的步骤,设为private防止子类调用
private:
void fryEgg() {
cout << "热油下锅,倒入蛋液,快速翻炒至凝固盛出" << endl;
}
void fryTomato() {
cout << "锅中加少许油,倒入番茄块,翻炒至出汁" << endl;
}
void mixAndFry() {
cout << "将炒好的鸡蛋倒回锅中,与番茄混合翻炒30秒" << endl;
}
void serve() {
cout << "将番茄炒蛋盛入盘中,完成!" << endl;
cout << "-------------------------" << endl;
}
};
步骤2:实现具体子类(不同口味)
每个子类仅需关注自身的差异化步骤,无需关心整体流程,代码复用率大幅提升。
// 具体子类1:原味番茄炒蛋
class OriginalTomatoEgg : public TomatoEggRecipe {
protected:
void prepareIngredients() override {
cout << "【原味】准备食材:番茄2个(不去皮)、鸡蛋3个(打散)" << endl;
}
void season() override {
cout << "加入食盐3克,翻炒均匀" << endl;
}
};
// 具体子类2:酸甜番茄炒蛋
class SweetSourTomatoEgg : public TomatoEggRecipe {
protected:
void prepareIngredients() override {
cout << "【酸甜味】准备食材:番茄2个(去皮切块)、鸡蛋3个(加少许糖打散)" << endl;
}
void season() override {
cout << "加入白糖10克、食盐2克、番茄酱5克,翻炒均匀" << endl;
}
};
// 具体子类3:辣味番茄炒蛋
class SpicyTomatoEgg : public TomatoEggRecipe {
protected:
void prepareIngredients() override {
cout << "【辣味】准备食材:番茄2个(去皮)、鸡蛋3个、小米辣2个(切碎)" << endl;
}
void season() override {
cout << "加入食盐3克、辣椒粉2克、小米辣,翻炒均匀" << endl;
}
};
步骤3:客户端调用(测试代码)
客户端只需面向抽象类编程,通过创建不同子类对象,即可得到不同口味的结果,符合"依赖倒置原则"。
int main() {
cout << "=== 开始制作番茄炒蛋 ===" << endl;
// 制作原味番茄炒蛋
TomatoEggRecipe* original = new OriginalTomatoEgg();
original->makeTomatoEgg();
// 制作酸甜味番茄炒蛋
TomatoEggRecipe* sweetSour = new SweetSourTomatoEgg();
sweetSour->makeTomatoEgg();
// 制作辣味番茄炒蛋
TomatoEggRecipe* spicy = new SpicyTomatoEgg();
spicy->makeTomatoEgg();
// 释放内存
delete original;
delete sweetSour;
delete spicy;
return 0;
}
运行结果与分析
=== 开始制作番茄炒蛋 ===
【原味】准备食材:番茄2个(不去皮)、鸡蛋3个(打散)
热油下锅,倒入蛋液,快速翻炒至凝固盛出
锅中加少许油,倒入番茄块,翻炒至出汁
将炒好的鸡蛋倒回锅中,与番茄混合翻炒30秒
加入食盐3克,翻炒均匀
将番茄炒蛋盛入盘中,完成!
-------------------------
【酸甜味】准备食材:番茄2个(去皮切块)、鸡蛋3个(加少许糖打散)
热油下锅,倒入蛋液,快速翻炒至凝固盛出
锅中加少许油,倒入番茄块,翻炒至出汁
将炒好的鸡蛋倒回锅中,与番茄混合翻炒30秒
加入白糖10克、食盐2克、番茄酱5克,翻炒均匀
将番茄炒蛋盛入盘中,完成!
-------------------------
【辣味】准备食材:番茄2个(去皮)、鸡蛋3个、小米辣2个(切碎)
热油下锅,倒入蛋液,快速翻炒至凝固盛出
锅中加少许油,倒入番茄块,翻炒至出汁
将炒好的鸡蛋倒回锅中,与番茄混合翻炒30秒
加入食盐3克、辣椒粉2克、小米辣,翻炒均匀
将番茄炒蛋盛入盘中,完成!
-------------------------
从结果可见:所有口味的制作流程完全一致,但差异化步骤按子类逻辑执行。如果需要新增"芝士味",只需创建新子类实现两个抽象方法,无需修改原有代码,完美符合"开闭原则"。
2.2 进阶案例:带钩子方法的数据处理框架
基础案例中我们用了抽象方法实现强制差异化,但实际开发中常有"可选步骤"——比如数据处理时,部分场景需要加密,部分不需要。这时候就需要"钩子方法"来实现灵活控制。
需求:设计通用数据处理框架,流程为"读取数据→验证数据→处理数据→[可选加密]→保存数据",其中加密步骤为可选,由子类决定是否执行。
步骤1:定义带钩子的抽象类
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 抽象类:数据处理框架
class DataProcessor {
public:
// 模板方法:数据处理完整流程
void processData(const string& source) final {
cout << "=== 开始处理来自[" << source << "]的数据 ===" << endl;
vector<string> data = readData(source); // 读取数据(强制差异化)
if (!validateData(data)) { // 验证数据(固定)
cout << "数据验证失败,终止处理" << endl;
return;
}
vector<string> processed = handleData(data);// 处理数据(强制差异化)
if (needEncrypt()) { // 钩子方法:判断是否需要加密
processed = encryptData(processed); // 加密数据(可选差异化)
}
saveData(processed); // 保存数据(强制差异化)
cout << "数据处理完成" << endl;
cout << "-------------------------" << endl;
}
protected:
// 抽象方法:强制子类实现的核心步骤
virtual vector<string> readData(const string& source) = 0;
virtual vector<string> handleData(const vector<string>& data) = 0;
virtual void saveData(const vector<string>& data) = 0;
// 钩子方法1:控制可选步骤(默认返回false,即不执行加密)
virtual bool needEncrypt() {
return false;
}
// 钩子方法2:可选实现的步骤(默认空实现,子类按需重写)
virtual vector<string> encryptData(const vector<string>& data) {
vector<string> encrypted;
for (const auto& item : data) {
// 默认简单加密:在每个数据前加"Enc_"
encrypted.push_back("Enc_" + item);
}
return encrypted;
}
// 固定方法:所有子类共享的数据验证逻辑
private:
bool validateData(const vector<string>& data) {
if (data.empty()) {
cout << "错误:读取到空数据" << endl;
return false;
}
cout << "数据验证通过,共" << data.size() << "条记录" << endl;
return true;
}
};
步骤2:实现具体子类(文件/数据库数据处理)
// 具体子类1:文件数据处理器(不需要加密)
class FileDataProcessor : public DataProcessor {
protected:
vector<string> readData(const string& source) override {
cout << "1. 从文件[" << source << "]读取数据" << endl;
// 模拟读取数据
return {"user1,18", "user2,20", "user3,22"};
}
vector<string> handleData(const vector<string>& data) override {
cout << "2. 处理文件数据:提取用户名" << endl;
vector<string> result;
for (const auto& item : data) {
result.push_back(item.substr(0, item.find(',')));
}
return result;
}
void saveData(const vector<string>& data) override {
cout << "3. 将处理后的数据保存到本地文件" << endl;
for (const auto& item : data) {
cout << "- " << item << endl;
}
}
// 无需加密,使用钩子方法默认实现
};
// 具体子类2:数据库数据处理器(需要加密)
class DatabaseDataProcessor : public DataProcessor {
protected:
vector<string> readData(const string& source) override {
cout << "1. 从数据库[" << source << "]读取数据" << endl;
// 模拟读取敏感数据
return {"admin,123456", "manager,654321"};
}
vector<string> handleData(const vector<string>& data) override {
cout << "2. 处理数据库数据:提取账号信息" << endl;
return data; // 敏感数据不做拆分,直接处理
}
void saveData(const vector<string>& data) override {
cout << "3. 将加密后的数据保存到备份数据库" << endl;
for (const auto& item : data) {
cout << "- " << item << endl;
}
}
// 重写钩子方法1:启用加密
bool needEncrypt() override {
return true;
}
// 重写钩子方法2:使用自定义加密算法
vector<string> encryptData(const vector<string>& data) override {
cout << "4. 执行自定义加密算法(替换敏感字符)" << endl;
vector<string> encrypted;
for (const auto& item : data) {
string temp = item;
// 简单模拟:将密码部分替换为*
size_t pos = temp.find(',');
if (pos != string::npos) {
temp.replace(pos+1, string::npos, string(temp.size()-pos-1, '*'));
}
encrypted.push_back(temp);
}
return encrypted;
}
};
步骤3:客户端测试
int main() {
// 处理文件数据(不加密)
DataProcessor* fileProcessor = new FileDataProcessor();
fileProcessor->processData("user_info.txt");
// 处理数据库数据(加密)
DataProcessor* dbProcessor = new DatabaseDataProcessor();
dbProcessor->processData("user_db");
delete fileProcessor;
delete dbProcessor;
return 0;
}
运行结果与钩子方法解析
=== 开始处理来自[user_info.txt]的数据 ===
1. 从文件[user_info.txt]读取数据
数据验证通过,共3条记录
2. 处理文件数据:提取用户名
3. 将处理后的数据保存到本地文件
- user1
- user2
- user3
数据处理完成
-------------------------
=== 开始处理来自[user_db]的数据 ===
1. 从数据库[user_db]读取数据
数据验证通过,共2条记录
2. 处理数据库数据:提取账号信息
4. 执行自定义加密算法(替换敏感字符)
3. 将加密后的数据保存到备份数据库
- admin,******
- manager,******
数据处理完成
-------------------------
钩子方法的核心价值在于"灵活控制流程":
-
FileDataProcessor未重写needEncrypt,默认不执行加密步骤;
-
DatabaseDataProcessor重写needEncrypt返回true,触发加密,同时重写encryptData实现自定义加密逻辑;
-
如果后续需要新增"日志数据处理器",只需决定是否重写钩子方法,扩展性极强。
2.3 工业级案例:日志框架的模板方法设计
在实际项目中,模板方法模式常用于框架设计,比如日志框架。不同日志输出方式(控制台、文件、网络)的流程一致,但输出细节不同,且需要支持日志级别过滤等公共逻辑。
完整实现代码
#include <iostream>
#include <string>
#include <fstream>
#include <ctime>
using namespace std;
// 日志级别枚举
enum class LogLevel {
DEBUG,
INFO,
WARN,
ERROR
};
// 抽象类:日志记录器模板
class Logger {
public:
// 模板方法:日志记录完整流程
void log(LogLevel level, const string& message) final {
if (!isLoggable(level)) { // 日志级别过滤(固定)
return;
}
string timestamp = getTimestamp(); // 获取时间戳(固定)
string levelStr = getLevelString(level); // 级别转换(固定)
string logMsg = formatLog(timestamp, levelStr, message); // 格式化(强制差异化)
writeLog(logMsg); // 写入日志(强制差异化)
}
// 设置日志级别(公共方法)
void setLogLevel(LogLevel level) {
m_logLevel = level;
}
protected:
LogLevel m_logLevel = LogLevel::INFO; // 默认日志级别为INFO
// 抽象方法:强制子类实现
virtual string formatLog(const string& timestamp, const string& level, const string& msg) = 0;
virtual void writeLog(const string& logMsg) = 0;
// 固定方法:日志级别过滤逻辑
private:
bool isLoggable(LogLevel level) {
return level >= m_logLevel;
}
// 固定方法:获取当前时间戳
string getTimestamp() {
time_t now = time(nullptr);
char buf[20];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", localtime(&now));
return buf;
}
// 固定方法:日志级别转换为字符串
string getLevelString(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARN: return "WARN";
case LogLevel::ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
};
// 具体子类1:控制台日志记录器
class ConsoleLogger : public Logger {
protected:
string formatLog(const string& timestamp, const string& level, const string& msg) override {
// 控制台日志格式:时间 [级别] 消息
return timestamp + " [" + level + "] " + msg;
}
void writeLog(const string& logMsg) override {
// 控制台输出,ERROR级别用红色
if (logMsg.find("[ERROR]") != string::npos) {
cout << "\033[31m" << logMsg << "\033[0m" << endl;
} else {
cout << logMsg << endl;
}
}
};
// 具体子类2:文件日志记录器
class FileLogger : public Logger {
public:
FileLogger(const string& filename) : m_filename(filename) {}
protected:
string formatLog(const string& timestamp, const string& level, const string& msg) override {
// 文件日志格式:时间|级别|消息(便于后续解析)
return timestamp + "|" + level + "|" + msg;
}
void writeLog(const string& logMsg) override {
// 追加写入文件
ofstream file(m_filename, ios::app);
if (file.is_open()) {
file << logMsg << endl;
file.close();
} else {
cerr << "无法打开日志文件:" << m_filename << endl;
}
}
private:
string m_filename; // 日志文件名
};
// 客户端测试
int main() {
// 1. 控制台日志测试
ConsoleLogger consoleLogger;
consoleLogger.setLogLevel(LogLevel::DEBUG); // 输出所有级别日志
consoleLogger.log(LogLevel::DEBUG, "初始化配置文件");
consoleLogger.log(LogLevel::INFO, "用户admin登录成功");
consoleLogger.log(LogLevel::WARN, "磁盘空间不足50%");
consoleLogger.log(LogLevel::ERROR, "数据库连接超时");
// 2. 文件日志测试
FileLogger fileLogger("app.log");
fileLogger.setLogLevel(LogLevel::INFO); // 不输出DEBUG级别
fileLogger.log(LogLevel::DEBUG, "这行日志不会被记录");
fileLogger.log(LogLevel::INFO, "系统启动完成");
fileLogger.log(LogLevel::ERROR, "支付接口调用失败");
return 0;
}
代码特点与工业级设计思路
这个日志框架体现了模板方法模式在工业级开发中的核心价值:
-
流程标准化:所有日志记录都遵循"过滤→时间戳→格式化→写入"的流程,确保日志输出规范;
-
扩展灵活:新增"网络日志记录器(发送到ELK)"时,只需实现formatLog和writeLog,无需修改框架核心逻辑;
-
职责单一:抽象类负责流程控制,子类专注于具体输出,符合"单一职责原则";
-
可配置性:通过setLogLevel方法统一控制日志级别,公共配置集中管理。
三、模板方法模式的核心价值与适用场景

通过前面的案例,我们已经掌握了模板方法模式的实现,但更重要的是理解它在什么场景下能发挥最大价值。盲目使用设计模式会导致代码冗余,精准匹配场景才是关键。
3.1 核心价值:解决什么问题?
模板方法模式的本质是"分离变与不变",它解决了"相同流程下,重复代码过多"和"流程一致性难以保证"两大痛点。
| 问题场景 | 无模式解决方案 | 模板方法模式解决方案 |
|---|---|---|
| 多个类流程相同,细节不同 | 每个类重复编写流程代码,冗余度高,修改流程需改所有类 | 流程集中在父类,子类仅实现细节,修改流程只需改父类 |
| 需要强制统一流程 | 依赖开发规范约束,易出现流程混乱(如先保存后验证) | 模板方法用final固定流程,子类无法修改,确保一致性 |
| 可选步骤灵活控制 | 用大量if-else判断,代码臃肿,不易维护 | 通过钩子方法实现,子类按需重写,代码清晰 |
3.2 典型适用场景
当你的代码符合以下特征时,就可以考虑使用模板方法模式:
-
框架设计场景:如Spring的生命周期(初始化→运行→销毁)、JUnit的测试流程(@Before→@Test→@After),框架定义流程,用户实现具体逻辑;
-
流程标准化场景:如订单处理(创建订单→支付→库存扣减→物流通知)、报表生成(查询数据→计算→格式化→导出);
-
多实现类共享逻辑场景:如各种解析器(XML/JSON/CSV),都有"读取文件→解析内容→验证格式→返回数据"的流程;
-
需要反向控制场景:父类需要调用子类的方法,而非子类调用父类,符合好莱坞原则。
3.3 优缺点客观分析
没有完美的设计模式,只有合适的设计模式。模板方法模式的优缺点同样鲜明,使用前需做好权衡。
优点
-
代码复用率高:公共逻辑集中在父类,避免子类重复编码;
-
流程一致性强:模板方法固定流程,子类无法破坏,减少人为错误;
-
扩展性好:新增实现只需创建子类,符合开闭原则;
-
逻辑清晰:父类控制流程,子类关注细节,职责边界明确。
缺点
-
类数量增加:每个实现都需创建子类,可能导致类膨胀(可通过组合模式缓解);
-
继承耦合度高:子类依赖父类的流程设计,父类修改可能影响所有子类;
-
流程修改成本高:如果模板方法的流程需要调整,父类的修改会涉及所有子类的兼容性测试。
四、模板方法模式与易混淆模式的区别

很多开发者会混淆模板方法模式与策略模式、工厂方法模式,它们都属于行为型模式,但解决的问题完全不同。下面通过表格清晰对比:
4.1 与策略模式的核心区别
两者都用于处理"算法变化",但变化的粒度和控制方式不同。
| 对比维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 核心思想 | 固定流程,变化步骤(纵向扩展) | 封装不同算法,动态替换(横向替换) |
| 流程控制 | 父类控制流程,子类无流程控制权 | 客户端控制算法选择,算法间独立 |
| 代码结构 | 继承关系,子类依赖父类 | 组合关系,客户端依赖接口 |
| 适用场景 | 流程固定,步骤可变 | 算法独立,需动态切换 |
| 举例 | 不同格式的报表导出(流程相同,格式不同) | 不同的支付方式(支付宝/微信/银联,算法独立) |
4.2 与工厂方法模式的核心区别
工厂方法模式是模板方法模式的特殊形式,专注于"对象创建"。
| 对比维度 | 模板方法模式 | 工厂方法模式 |
|---|---|---|
| 核心目标 | 规范算法流程 | 规范对象创建 |
| 抽象方法作用 | 实现算法步骤 | 创建具体对象 |
| 模板方法内容 | 完整的业务流程 | 对象创建的固定逻辑(如初始化配置) |
| 关系 | 工厂方法模式是模板方法模式的子集 | 专注于对象创建的模板方法 |
五、C++实现模板方法模式的避坑指南

C++的特性(如继承、虚函数、访问控制)为模板方法模式提供了强大支持,但也容易踩坑。下面是实战中总结的8个关键技巧:
5.1 模板方法务必加final修饰
这是最容易忽略也最关键的一点。如果模板方法不加final,子类可能会重写它,破坏整个流程的一致性。
// 错误:未加final,子类可重写
virtual void templateMethod() { ... }
// 正确:加final防止重写
void templateMethod() final { ... }
5.2 合理控制方法访问权限
访问权限的设计直接影响模式的安全性,遵循以下原则:
-
模板方法:public(供客户端调用)+ final;
-
抽象方法/钩子方法:protected(子类可重写,但不暴露给客户端);
-
固定流程方法:private(子类不可调用,避免破坏流程)。
5.3 钩子方法要提供默认实现
钩子方法是"可选步骤",必须用虚函数并提供默认实现(空实现或默认逻辑),否则子类会被迫重写,失去钩子的灵活性。
// 正确的钩子方法
virtual bool needEncrypt() { return false; }
// 错误的钩子方法(子类必须重写)
virtual bool needEncrypt() = 0;
5.4 避免模板方法过于庞大
如果模板方法中的步骤过多(如超过5个),会导致抽象类职责过重。解决方案:将多个相关步骤封装为一个"子流程方法",保持模板方法简洁。
// 优化前:步骤臃肿
void templateMethod() final {
step1();
step2();
step3();
step4();
step5();
step6();
}
// 优化后:封装子流程
void templateMethod() final {
prepareStep(); // 封装step1-step2
processStep(); // 封装step3-step4
finishStep(); // 封装step5-step6
}
// 子流程方法设为private
private:
void prepareStep() { step1(); step2(); }
void processStep() { step3(); step4(); }
void finishStep() { step5(); step6(); }
5.5 结合泛型提升灵活性
对于数据处理类的模板方法,可以结合C++泛型(模板类),支持不同数据类型,无需为每种类型创建抽象类。
// 泛型抽象类
template <typename T>
class DataProcessor {
public:
void process() final {
T data = read();
handle(data);
save(data);
}
protected:
virtual T read() = 0;
virtual void handle(T& data) = 0;
virtual void save(const T& data) = 0;
};
// 处理int类型数据
class IntDataProcessor : public DataProcessor<int> { ... };
// 处理string类型数据
class StringDataProcessor : public DataProcessor<string> { ... };
5.6 避免在构造函数中调用虚函数
C++中,构造函数执行时子类还未初始化,此时调用虚函数会执行父类版本,导致逻辑错误。模板方法模式中,模板方法绝不能在构造函数中调用。
// 错误:构造函数中调用模板方法
class AbstractClass {
public:
AbstractClass() {
templateMethod(); // 此时子类未初始化,虚函数调用父类版本
}
void templateMethod() final { ... }
};
// 正确:客户端手动调用模板方法
int main() {
ConcreteClass obj;
obj.templateMethod(); // 子类已初始化,正常调用
}
5.7 用组合缓解继承耦合
模板方法模式依赖继承,耦合度较高。如果子类数量过多,可以结合组合模式:将可变步骤封装为独立的策略类,在模板方法中通过组合调用。
5.8 文档化模板流程
在抽象类的注释中,必须清晰说明模板方法的执行顺序、各步骤的作用,以及钩子方法的控制逻辑,便于后续开发者实现子类。
六、总结:模板方法模式的本质与思考
模板方法模式看似简单,却是很多框架的核心设计思想。它没有引入复杂的结构,仅仅通过"继承+虚函数"的组合,就实现了"流程复用与灵活扩展"的平衡。
回顾本文的核心观点:
-
核心思想:父类定流程,子类填细节,遵循好莱坞原则;
-
关键组件:模板方法(final)、抽象方法(强制实现)、钩子方法(可选实现);
-
适用场景:流程固定但步骤可变的框架或业务场景;
-
C++实现要点:控制访问权限、模板方法加final、钩子提供默认实现。
最后给大家一个实用建议:在编写重复代码前,先思考"这些代码的流程是否一致?差异点在哪里?"如果符合"流程固定、细节可变"的特征,模板方法模式就是你的最优选择。它不仅能减少代码冗余,更能让你的系统结构更清晰、扩展性更强。

被折叠的 条评论
为什么被折叠?



