模板方法模式:算法骨架,子类实现细节
模板方法模式,说白了就是「把不变的部分写死,把可变的部分留给子类」。我刚开始接触设计模式时,觉得这玩意儿太简单了——不就是虚函数加个基类吗?后来在项目中真正用起来,才发现它的威力远不止于此。
你想想看,框架设计里最头疼的是什么?是「让用户能定制,但又不能让他们把框架搞坏」。模板方法模式正好解决了这个问题:父类定义好流程,子类只能填空,不能改流程。
模式本质:好莱坞原则
「别打电话给我们,我们会打给你。」——这就是好莱坞原则,也是模板方法模式的核心思想。
父类控制整个算法的执行流程,子类只需要实现某些步骤。父类在合适的时机调用子类的方法,子类不能反过来控制父类的流程。
- 抽象类(AbstractClass):定义算法骨架,包含模板方法和若干抽象/钩子方法
- 具体类(ConcreteClass):实现抽象方法,可选地重写钩子方法
- 模板方法(Template Method):定义算法步骤,通常声明为 final(C++中非虚函数)
一个真实的例子:数据导出框架
我记得之前做一个报表系统,需要支持导出成 CSV、Excel、PDF 三种格式。每种格式的导出步骤其实差不多:
- 打开数据源
- 读取数据
- 格式化数据
- 写入文件
- 关闭资源
你看,步骤 1、2、5 是通用的,步骤 3、4 才是差异点。用模板方法模式再合适不过了。
class DataExporter {
public:
// 模板方法——定义算法骨架
void exportData(const std::string& filePath) {
openDataSource(); // 通用
auto data = readData(); // 通用
auto formatted = formatData(data); // 子类实现
writeToFile(formatted, filePath); // 子类实现
closeDataSource(); // 通用
}
virtual ~DataExporter() = default;
protected:
// 抽象方法——子类必须实现
virtual std::string formatData(const std::vector<Record>& data) = 0;
virtual void writeToFile(const std::string& content, const std::string& path) = 0;
// 钩子方法——子类可选重写
virtual bool needHeader() const { return true; }
private:
// 通用步骤——子类不可重写
void openDataSource() {
// 连接数据库、打开文件等
}
std::vector<Record> readData() {
// 执行查询、读取数据
}
void closeDataSource() {
// 释放资源、关闭连接
}
};
这里有个细节:模板方法 exportData 我声明为普通成员函数,不是虚函数。为什么?因为我不希望子类重写整个流程,子类只能填空,不能改流程。
钩子方法:给子类留点余地
有时候,某些步骤不是必须的。比如 CSV 导出可能需要表头,但 PDF 导出不需要。这时候钩子方法就派上用场了。
class CsvExporter : public DataExporter {
protected:
std::string formatData(const std::vector<Record>& data) override {
std::string result;
if (needHeader()) {
result += "Name,Age,Email\n";
}
for (const auto& record : data) {
result += record.name + "," +
std::to_string(record.age) + "," +
record.email + "\n";
}
return result;
}
void writeToFile(const std::string& content, const std::string& path) override {
std::ofstream file(path);
file << content;
}
};
class PdfExporter : public DataExporter {
protected:
std::string formatData(const std::vector<Record>& data) override {
// PDF 有自己的格式化逻辑
}
void writeToFile(const std::string& content, const std::string& path) override {
// 使用 PDF 库写入
}
bool needHeader() const override { return false; } // PDF 不需要表头
};
框架设计中的应用
模板方法模式在 C++ 框架中随处可见。我随便举几个例子:
| 框架/库 | 模板方法 | 子类实现 |
|---|---|---|
| MFC | CWinApp::Run() | InitInstance(), ExitInstance() |
| Qt | QWidget::paintEvent() | paint() 中的绘制逻辑 |
| STL | std::sort 的比较器 | operator< 或 lambda |
| 单元测试框架 | Test::run() | setUp(), tearDown(), testMethod() |
你看,这些框架的核心思想都一样:框架控制流程,用户提供实现。这就是模板方法模式的精髓。
避坑指南
- 模板方法里调用虚函数:如果在构造函数或析构函数中调用虚函数,C++ 不会按你期望的方式分发。子类还没构造完呢!
- 过度使用钩子方法:钩子方法太多,子类实现起来很痛苦。我一般控制在 2-3 个以内。
- 模板方法太复杂:如果一个模板方法超过 20 行,说明你可能需要拆分了。算法骨架应该清晰易懂。
与策略模式的对比
很多人会把模板方法和策略模式搞混。我简单说一下区别:
- 模板方法:继承实现,子类改写父类的某些步骤。适合「大部分相同,小部分不同」的场景。
- 策略模式:组合实现,通过接口注入不同的算法。适合「算法完全可替换」的场景。
举个例子:做咖啡和茶,步骤差不多(烧水、冲泡、倒杯、加料),只是冲泡和加料不同——用模板方法。但如果你有多种支付方式(支付宝、微信、银行卡),每种支付方式完全独立——用策略模式。
SVG 结构图
总结
模板方法模式,说白了就是「框架思维」。你写的是框架,用户写的是插件。你控制流程,用户提供细节。
我个人觉得,这个模式最大的价值不在于代码复用,而在于「约定大于配置」。它明确告诉使用者:哪些地方你可以改,哪些地方你不能碰。这种清晰的边界,在大型项目中尤其重要。
嗯,最后说一句:别把模板方法搞得太复杂。一个好的模板方法,读起来应该像一篇清晰的菜谱——步骤明确,顺序固定,留给厨师的发挥空间恰到好处。
公众号:蓝海资料掘金营,微信 deep3321