模板方法模式:模式动机与定义
模板方法模式,说白了就是「把不变的部分封装起来,把变化的部分交给子类」。我刚开始接触设计模式时,总觉得这玩意儿太简单了——不就是虚函数加个骨架方法吗?后来在真实项目中踩过坑才明白,这个模式其实是代码复用和框架设计的基石。
你想想看,我们在写代码时经常遇到这种情况:多个类做同一件事,步骤差不多,但中间某几步的具体实现不一样。比如数据导出功能,不管是导出成PDF、Excel还是CSV,流程都是「打开文件→写入数据→关闭文件」。但写入数据这一步,不同格式的处理方式完全不同。
模板方法模式就是来解决这个问题的。它定义了一个操作中的算法骨架,而将一些步骤延迟到子类中实现。父类控制流程,子类填充细节。
核心定义:模板方法模式(Template Method Pattern)定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
角色与结构
这个模式的结构其实很清晰,就三个角色:
- 抽象类(AbstractClass):定义模板方法和基本操作。模板方法规定了算法骨架,基本操作是子类需要实现的步骤。
- 具体类(ConcreteClass):实现抽象类中定义的基本操作。不同的具体类有不同的实现方式。
- 客户端(Client):调用模板方法,完成整个流程。
我记得有一次在重构一个支付系统时,发现不同支付渠道(微信、支付宝、银联)的支付流程几乎一模一样:创建订单→签名→发起支付→处理回调→更新订单状态。唯一不同的就是签名算法和回调处理逻辑。这不就是模板方法模式的典型场景吗?
下面这张图展示了模板方法模式的核心结构:
C++代码实现
光说不练假把式。咱们直接上代码,看看模板方法模式在C++里怎么写。
先定义一个数据导出器的抽象类:
// DataExporter.h
class DataExporter {
public:
virtual ~DataExporter() = default;
// 模板方法 - 定义导出流程骨架
void exportData(const std::string& data) {
openFile();
writeHeader();
writeBody(data);
writeFooter();
closeFile();
if (needCompress()) {
compressFile();
}
}
protected:
// 基本操作 - 子类可重写
virtual void openFile() {
std::cout << "打开默认文件..." << std::endl;
}
virtual void writeHeader() {
// 默认实现,子类可覆盖
}
virtual void writeBody(const std::string& data) = 0; // 纯虚函数
virtual void writeFooter() {
// 默认实现
}
virtual void closeFile() {
std::cout << "关闭文件..." << std::endl;
}
// 钩子方法 - 可选扩展点
virtual bool needCompress() {
return false; // 默认不压缩
}
virtual void compressFile() {
std::cout << "压缩文件..." << std::endl;
}
};
然后实现两个具体类:
// PdfExporter.h
class PdfExporter : public DataExporter {
protected:
void openFile() override {
std::cout << "创建PDF文档..." << std::endl;
}
void writeHeader() override {
std::cout << "写入PDF头部信息..." << std::endl;
}
void writeBody(const std::string& data) override {
std::cout << "将数据转换为PDF格式: " << data << std::endl;
}
void writeFooter() override {
std::cout << "写入PDF页脚..." << std::endl;
}
bool needCompress() override {
return true; // PDF文件通常需要压缩
}
};
// CsvExporter.h
class CsvExporter : public DataExporter {
protected:
void writeBody(const std::string& data) override {
std::cout << "将数据转换为CSV格式: " << data << std::endl;
}
// CSV不需要header和footer,保持默认空实现
};
客户端调用:
// main.cpp
int main() {
std::unique_ptr<DataExporter> exporter;
exporter = std::make_unique<PdfExporter>();
exporter->exportData("用户报表数据");
std::cout << "---" << std::endl;
exporter = std::make_unique<CsvExporter>();
exporter->exportData("用户报表数据");
return 0;
}
个人经验:我在实际项目中,经常用钩子方法(hook)来处理那些「有时候需要,有时候不需要」的步骤。比如上面的needCompress(),默认返回false,子类按需重写。这样既保持了骨架的完整性,又给了子类足够的灵活性。
框架设计应用
模板方法模式在框架设计中无处不在。我参与过几个C++框架的开发,这个模式几乎是标配。
1. 游戏引擎中的更新循环
游戏引擎的每一帧更新流程是固定的:处理输入→更新逻辑→渲染画面。但具体怎么更新、怎么渲染,每个游戏都不一样。
class GameEngine {
public:
void run() {
while (isRunning) {
handleInput(); // 子类实现
update(); // 子类实现
render(); // 子类实现
}
}
protected:
virtual void handleInput() = 0;
virtual void update() = 0;
virtual void render() = 0;
};
2. 单元测试框架
你想想看,Google Test、Catch2这些测试框架,本质上都是模板方法模式。测试流程是固定的:setUp→testBody→tearDown。用户只需要实现testBody,框架帮你管理生命周期。
3. 网络协议栈
我记得在做一个物联网项目时,需要支持多种通信协议(MQTT、CoAP、HTTP)。每种协议的连接建立、数据收发、连接关闭流程都不同,但整体骨架是一样的。用模板方法模式,我们把协议处理流程固定下来,具体实现交给子类。
框架设计原则:好莱坞原则——「不要调用我们,我们会调用你」。模板方法模式正是这个原则的典型体现。框架控制流程,用户代码被框架调用。
避坑指南:我曾经在一个项目中过度使用模板方法模式,把整个业务逻辑都塞进了一个大模板方法里,结果那个方法有十几个步骤,子类重写起来痛苦不堪。记住:模板方法里的步骤粒度要适中,太细了子类要重写太多方法,太粗了又失去了灵活性。我个人的经验是,一个模板方法里3-5个步骤比较合适。
4. 数据库访问层
数据库操作通常遵循:连接→执行SQL→处理结果→关闭连接。不同数据库(MySQL、PostgreSQL、SQLite)的具体实现不同,但流程一致。
class DatabaseAccessor {
public:
void executeQuery(const std::string& sql) {
connect();
prepareStatement(sql);
execute();
processResult();
disconnect();
}
protected:
virtual void connect() = 0;
virtual void prepareStatement(const std::string& sql) = 0;
virtual void execute() = 0;
virtual void processResult() = 0;
virtual void disconnect() = 0;
};
总结
模板方法模式的核心价值在于:把不变的部分固化在父类中,把变化的部分留给子类。它避免了代码重复,也保证了流程的一致性。
我个人觉得,这个模式特别适合以下场景:
- 多个类有相似的算法流程,只是某些步骤不同
- 需要控制子类的扩展点,防止子类破坏算法结构
- 框架设计中需要提供扩展点给用户
嗯,最后说一句:模板方法模式虽然简单,但用好了能让代码结构清晰很多。别因为它简单就轻视它,我在很多大型项目中都见过它的身影。
公众号:蓝海资料掘金营,微信deep3321