16、模板方法模式:模式动机与定义、角色与结构、C++代码实现、好莱坞原则

16.1 模式动机——为什么需要模板方法?

先说说我自己的经历。几年前我接手一个支付系统,里面有微信支付、支付宝、银联三种渠道。每个渠道的支付流程其实差不多:参数校验、签名、发起请求、处理响应、回调通知。但每个渠道的具体实现细节完全不同。

一开始,团队里的小伙子们各自为政。每个人写一套完整的支付流程。结果呢?代码大量重复。更糟的是,后来要加一个「支付日志」功能,得改三个地方。漏改一个,线上就出问题。

你想想看,这种场景是不是很常见?

模板方法模式就是来解决这个问题的。它把不变的流程骨架固定下来,把可变的部分留给子类去实现。说白了,就是「流程标准化,细节差异化」。

核心思想: 在基类中定义一个算法的骨架,将某些步骤延迟到子类中实现。子类可以重新定义算法的特定步骤,但不能改变算法的整体结构。

16.2 模式定义与角色结构

模板方法模式的定义其实很直白:

定义一个操作中的算法骨架,将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些特定步骤。

这里面涉及两个主要角色:

角色 名称 职责
AbstractClass 抽象基类 定义模板方法(算法骨架)和抽象步骤方法
ConcreteClass 具体子类 实现抽象步骤方法,完成特定逻辑

这里有个关键点:模板方法通常是 final 的(C++中虽然不是关键字,但设计上不应被重写)。子类只能重写那些被标记为「可定制」的步骤方法。

我的习惯: 在基类中,我会把模板方法声明为 non-virtual,把步骤方法声明为 protected virtual。这样既保证了骨架不被破坏,又给了子类足够的灵活性。

16.3 核心结构图

下面这张图展示了模板方法模式的核心结构。你看,流程是固定的,但每个步骤都可以有不同的实现。

AbstractClass(抽象基类) + templateMethod() : void [final] # step1() : void [virtual] # step2() : void [virtual] # step3() : void [virtual] # hook() : void [virtual, 可选] ConcreteClassA + step1() : void + step2() : void + step3() : void ConcreteClassB + step1() : void + step2() : void + step3() : void templateMethod() 执行流程 ① step1() — 参数校验 ② step2() — 签名/加密 ③ step3() — 发起请求 ④ hook() — 日志/扩展(可选) ⑤ 返回结果

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 使用场景与注意事项

什么时候该用模板方法模式?我总结了几个场景:

  • 多个子类有相同的算法结构 —— 比如各种支付渠道、各种文件解析器
  • 需要控制子类的扩展点 —— 只允许子类在特定位置插入逻辑
  • 避免代码重复 —— 把公共流程提取到基类

但也要注意:

我的建议: 不要滥用模板方法。如果子类之间的差异太大,强行用模板方法反而会让代码变得别扭。这时候可以考虑策略模式或者组合模式。另外,模板方法中的步骤不要太多,5-7个是合理的范围。超过10个,就该考虑拆分了。

嗯,模板方法模式就讲到这里。它不是什么高深的技术,但用好了,能让你的代码结构清晰很多。下次你写类似流程的代码时,不妨试试这个思路。


公众号:蓝海资料掘金营,微信deep3321