16、模板方法模式:模式动机与定义、角色与结构、C++代码实现、好莱坞原则
16.1 模式动机——为什么需要模板方法?
先说说我自己的经历。几年前我接手一个支付系统,里面有微信支付、支付宝、银联三种渠道。每个渠道的支付流程其实差不多:参数校验、签名、发起请求、处理响应、回调通知。但每个渠道的具体实现细节完全不同。
一开始,团队里的小伙子们各自为政。每个人写一套完整的支付流程。结果呢?代码大量重复。更糟的是,后来要加一个「支付日志」功能,得改三个地方。漏改一个,线上就出问题。
你想想看,这种场景是不是很常见?
模板方法模式就是来解决这个问题的。它把不变的流程骨架固定下来,把可变的部分留给子类去实现。说白了,就是「流程标准化,细节差异化」。
16.2 模式定义与角色结构
模板方法模式的定义其实很直白:
定义一个操作中的算法骨架,将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些特定步骤。
这里面涉及两个主要角色:
| 角色 | 名称 | 职责 |
|---|---|---|
| AbstractClass | 抽象基类 | 定义模板方法(算法骨架)和抽象步骤方法 |
| ConcreteClass | 具体子类 | 实现抽象步骤方法,完成特定逻辑 |
这里有个关键点:模板方法通常是 final 的(C++中虽然不是关键字,但设计上不应被重写)。子类只能重写那些被标记为「可定制」的步骤方法。
16.3 核心结构图
下面这张图展示了模板方法模式的核心结构。你看,流程是固定的,但每个步骤都可以有不同的实现。
16.4 C++代码实现
咱们直接看代码。还是拿支付系统举例:
// 抽象基类:支付流程
class PaymentProcessor {
public:
virtual ~PaymentProcessor() = default;
// 模板方法 —— 注意不是虚函数,子类不能重写
void processPayment(const PaymentRequest& req) {
// 步骤1:参数校验
if (!validateParams(req)) {
onError("参数校验失败");
return;
}
// 步骤2:签名
auto signedData = sign(req);
// 步骤3:发起请求
auto response = sendRequest(signedData);
// 步骤4:处理响应
handleResponse(response);
// 步骤5:钩子方法 —— 子类可选重写
afterPayment(response);
}
protected:
// 抽象步骤 —— 子类必须实现
virtual bool validateParams(const PaymentRequest& req) = 0;
virtual std::string sign(const PaymentRequest& req) = 0;
virtual PaymentResponse sendRequest(const std::string& data) = 0;
virtual void handleResponse(const PaymentResponse& resp) = 0;
// 钩子方法 —— 子类可选重写,默认空实现
virtual void afterPayment(const PaymentResponse& resp) {
// 默认什么都不做
}
virtual void onError(const std::string& msg) {
std::cerr << "支付失败: " << msg << std::endl;
}
};
// 具体子类:微信支付
class WeChatPay : public PaymentProcessor {
protected:
bool validateParams(const PaymentRequest& req) override {
// 微信特有的参数校验逻辑
return !req.getAppId().empty() && !req.getMchId().empty();
}
std::string sign(const PaymentRequest& req) override {
// 微信MD5签名
return md5(req.getAppId() + req.getMchId() + req.getAmount());
}
PaymentResponse sendRequest(const std::string& data) override {
// 调用微信HTTP API
return httpPost("https://api.weixin.qq.com/pay", data);
}
void handleResponse(const PaymentResponse& resp) override {
if (resp.getCode() == 200) {
std::cout << "微信支付成功" << std::endl;
}
}
// 微信支付需要记录日志,重写钩子
void afterPayment(const PaymentResponse& resp) override {
logToDatabase("wechat", resp.getTransactionId());
}
};
// 具体子类:支付宝支付
class Alipay : public PaymentProcessor {
protected:
bool validateParams(const PaymentRequest& req) override {
// 支付宝特有的参数校验
return !req.getAppId().empty() && req.getAmount() > 0;
}
std::string sign(const PaymentRequest& req) override {
// 支付宝RSA签名
return rsaSign(req.getAppId() + std::to_string(req.getAmount()));
}
PaymentResponse sendRequest(const std::string& data) override {
// 调用支付宝HTTP API
return httpPost("https://openapi.alipay.com/gateway.do", data);
}
void handleResponse(const PaymentResponse& resp) override {
if (resp.getCode() == "10000") {
std::cout << "支付宝支付成功" << std::endl;
}
}
};
16.5 好莱坞原则
说到模板方法模式,就不得不提好莱坞原则。这个原则的名字挺有意思——「不要给我们打电话,我们会打给你」。
什么意思呢?
在传统编程中,是高层代码调用底层代码。但在模板方法模式中,情况反过来了:
- 基类(高层) 控制着整个流程的节奏
- 子类(底层) 只负责实现具体的步骤
- 子类不能主动调用基类的方法,而是等待基类来调用自己
说白了,就是「控制权反转」。基类说:「流程我来管,你只需要告诉我每个步骤怎么做就行。」
- 避免「循环依赖」—— 子类不需要知道基类的完整逻辑
- 降低耦合度 —— 子类只关注自己的「一亩三分地」
- 提高复用性 —— 流程骨架写一次,到处用
我记得有一次重构一个老系统,里面到处都是 if-else 判断支付渠道。用了模板方法 + 好莱坞原则后,代码量减少了 40%。更重要的是,新来的人加一个新渠道,只需要继承基类,实现几个方法就行。完全不用担心破坏现有逻辑。
16.6 使用场景与注意事项
什么时候该用模板方法模式?我总结了几个场景:
- 多个子类有相同的算法结构 —— 比如各种支付渠道、各种文件解析器
- 需要控制子类的扩展点 —— 只允许子类在特定位置插入逻辑
- 避免代码重复 —— 把公共流程提取到基类
但也要注意:
嗯,模板方法模式就讲到这里。它不是什么高深的技术,但用好了,能让你的代码结构清晰很多。下次你写类似流程的代码时,不妨试试这个思路。
公众号:蓝海资料掘金营,微信deep3321