开放—封闭原则实战(下):基于策略的流程编排与编译期多态

前言:从"外循环分发"到"内循环编排"

在上篇 中,我们通过**"纯虚接口 + 泛型工厂 + 注册目录"的三位一体架构,彻底消灭了业务分发中心 dispatch() 里的 if-else,实现了 业务类型在运行时的即插即用**。

当请求被精准路由到具体的配送任务之后,很多 C++ 开发者会遇到第二个深层次的架构难题:

在每个具体业务任务内部,如何既能复用标准处理流程(参数解析 ➔ 规则鉴权 ➔ 执行 ➔ 序列化),又能在不修改公共流程的前提下,灵活替换各种校验规则?

很多初学者习惯性地继续掏出"虚函数",但 C++ 拥有 Java/Go 所不具备的杀手锏------编译期静态多态与基于策略的设计(Policy-Based Design)。

本篇将以苏格拉底问答的形式,带你从"微观流程编排"的视角,探索开放---封闭原则在编译期的极致应用。


第一层:业务内部流程的"样板代码"重复

学员: 在上篇中,我们独立实现了各个配送任务类。但我写着写着发现,每个具体任务类的内部流程几乎都在重复:

cpp 复制代码
int HomeDropTask::handle(const string& inputText, string& outputText)
{
    // 1. 检查地址与收货资格
    // 2. 将 inputText 反序列化为 HomeRequest 对象
    // 3. 执行送货上门的核心业务
    // 4. 将结果序列化为 outputText 返回
    return 0;
}

int ColdChainTask::handle(const string& inputText, string& outputText)
{
    // 1. 检查温控设备与冷链资格
    // 2. 将 inputText 反序列化为 ColdRequest 对象
    // 3. 执行冷链配送的核心业务
    // 4. 将结果序列化为 outputText 返回
    return 0;
}

每个类都在机械地重复这套流程,不仅写起来繁琐,一旦公共流程要加一个统一监控打点,所有类全都要改一遍。

讲师: 没错。这里暴露出了系统在微观层面的第二个变化特征:

主干处理流程高度稳定,但各业务的输入对象类型、输出对象类型以及前置检查规则不同。

这是经典的"流程骨架固定,局部细节多变"场景。


第二层:如果把校验写死在公共流程里,噩梦重演

学员: 既然流程固定,那我写一个公共的基类或者公共函数把流程包起来,检查规则直接写在里面行不行?

cpp 复制代码
// 糟糕的做法:又把 if-else 抓回来了!
int executeCommonFlow(int businessType, const string& in, string& out)
{
    // 公共检查逻辑
    if (businessType == TYPE_HOME) {
        checkAddress();
    } else if (businessType == TYPE_COLD) {
        checkTemperaturePermission();
    } else if (businessType == TYPE_CROSS_BORDER) {
        checkCustomsInformation(); // 跨境配送查报关单
    }
    
    // 执行业务...
}

讲师: 你看,你又把在上篇好不容易消灭的 if-else 给请回来了!

每当新增一种配送方式(比如无人机配送,需要检查空域管制),你又得被迫修改这个公共流程。这再次直接违背了开放---封闭原则。


第三层:用模板类封装稳定的流程骨架

学员: 面向对象里不是有"模板方法模式(Template Method)"吗?这里能不能用 C++ 的类模板来实现?

讲师: 完全可以!而且 C++ 模板可以让类型约束做得更加严丝合缝:

cpp 复制代码
// 通用配送流程骨架模板
template<
    class Incoming,  // 输入请求类型
    class Outgoing,  // 输出响应类型
    class Gate>      // 关键所在:前置检查规则(策略策略类)
class ShippingFlow : public IShippingTask
{
public:
    int handle(const string& inputText, string& outputText) override
    {
        // 步骤 1:执行专属于该业务的检查策略(Gate)
        Gate gate;
        int result = gate.verify(session_);
        if (result != 0) {
            return result; // 校验不通过,直接拦截
        }

        // 步骤 2:自动反序列化
        Incoming input;
        if (!input.parse(inputText)) {
            return -1;
        }

        // 步骤 3:调用业务子类实现的真正领域逻辑
        Outgoing output;
        result = execute(input, output);

        // 步骤 4:自动序列化响应文本
        outputText = output.serialize();
        return result;
    }

    // 留给真正业务实现的纯虚函数(仅处理强类型的输入输出)
    virtual int execute(Incoming& input, Outgoing& output) = 0;

protected:
    SessionInfo session_;
};

通过这个模板,整个流程被彻底固化:

复制代码
执行 Gate 校验规则 ➔ 反序列化为强类型 Input ➔ 调用业务 execute() ➔ 序列化 Output

公共流程代码被收敛在 ShippingFlow 中,零重复!


第四层:Policy-Based Design------检查规则的编译期无感替换

学员: 那么具体业务类该怎么写呢?

讲师: 业务类只需要声明自己需要什么输入、什么输出,以及绑定哪一个检查策略(Gate):

1. 独立编写各个检查规则策略类:
cpp 复制代码
// 上门检查策略:只关心地址合法性
class AddressGate {
public:
    int verify(SessionInfo& session) {
        // 检查送货地址、门牌号有效性
        return 0;
    }
};

// 冷链检查策略:只关心温控与冷库资质
class TemperatureGate {
public:
    int verify(SessionInfo& session) {
        // 检查冷链温度合规性与保鲜设备
        return 0;
    }
};
2. 像搭积木一样组装业务:
cpp 复制代码
// 上门配送任务:组装 AddressGate
class HomeDropTask : public ShippingFlow<HomeRequest, HomeResponse, AddressGate>
{
public:
    int execute(HomeRequest& input, HomeResponse& output) override {
        // 纯粹的送货上门业务逻辑,输入已经自动校验并转为强类型!
        return 0;
    }
};

// 冷链配送任务:组装 TemperatureGate
class ColdChainTask : public ShippingFlow<ColdRequest, ColdResponse, TemperatureGate>
{
public:
    int execute(ColdRequest& input, ColdResponse& output) override {
        // 纯粹的冷链配送业务逻辑
        return 0;
    }
};

你看这里发生了什么:

如果明天要增加一个"跨境配送(CrossBorderTask)",需要检查海关清关资质:

  1. 只需要写一个独立的 CustomsGate 策略类;
  2. 继承 ShippingFlow<..., CustomsGate>;
  3. 公共流程 ShippingFlow::handle() 一行代码都不需要改动!

这就是现代 C++ 经典著作《Modern C++ Design》中著名的 Policy-Based Design(基于策略的设计)。


第五层:深度拷问------Gate 为什么不需要继承一个虚基类?

学员: 我注意到 AddressGate 和 TemperatureGate 都是普通的独立类,并没有写 class AddressGate : public IGate。难道它不需要继承一个统一的抽象接口吗?

讲师: 完全不需要!这就是 C++ 模板相比 Java/Go 接口最震撼的地方。

在 Java 中,策略模式必须声明一个 interface Gate,然后让所有类实现它;

但在 C++ 模板中,遵循的是**"隐式接口约束(Compile-time Duck Typing / Concepts)"**:

在 ShippingFlow 中,编译器只要求这一行表达式在语法上成立:

cpp 复制代码
Gate gate;
int result = gate.verify(session_);

只要你的策略类满足以下契约:

  1. 能够无参构造;
  2. 拥有名为 verify 的公共成员函数;
  3. 能接收 SessionInfo& 参数,且返回值能转为 int。

它就能通过编译!

带来两大绝顶好处:
  1. 零虚函数开销,极致内联优化 :
    因为没有虚表指针(vptr),编译器可以在编译期直接将 gate.verify() **完整内联(Inline)**展开到执行流程中,性能损耗为 0!

  2. 编译期严格截断错误 :
    如果你写了一个非法的 Gate 类(比如方法名写成了 check() 而不是 verify()),或者返回值不匹配:

    cpp 复制代码
    class InvalidGate {
    public:
        void check(); // 方法名错了!
    };

    编译器会在编译期直接报错截断,根本不可能把低级错误带到生产环境运行。


第六层:动静结合的艺术------两种扩展点的本质取舍

学员: 整个系统看下来,为什么我们在上篇中对"配送任务(Task)"使用了虚函数与注册表 ,而在下篇中对"检查规则(Gate)"却使用了模板参数?为什么不统一都用模板,或者都用虚函数?

讲师: 这是整套架构最值得细细品味的地方------因为两者的"决策时机(Binding Time)"完全不同!

复制代码
                     系统中的两大变化点
                             │
        ┌────────────────────┴────────────────────┐
        ▼                                         ▼
【外部路由:Task 的选择】                  【内部规则:Gate 的选择】
请求到达网络网关时,报文里                在程序员编写具体任务类时,
动态包含业务编号 (serviceCode=1001)       就已经白纸黑字写死在代码里了
        │                                         │
        ▼                                         ▼
 必须在【运行时动态决定】                 可以在【编译期静态决定】
        │                                         │
        ▼                                         ▼
 适用武器:纯虚接口 + 注册表             适用武器:模板参数 (Policy 策略)
运行时多态 vs 编译期多态 决策对比矩阵:
考量维度 业务任务(IShippingTask) 内部校验规则(Gate Policy)
选择发生时间 运行时(Runtime):根据网络报文 ID 动态查表 编译时(Compile-time):类声明时模板参数绑定
技术实现 纯虚接口 + 泛型工厂 + 字典注册表 泛型模板参数(Policy-Based Design)
性能损耗 存在微小的虚表指针寻址(纳秒级) 零开销,代码直接内联优化
类型约束 显式继承父类契约(IShippingTask) 隐式签名契约(Duck Typing / C++20 Concepts)
新增扩展点 新增类 ➔ 调用 attach() 注册 新增类 ➔ 作为模板参数声明

架构大师的准则:

  • 能在编译期确定的东西,绝不拖到运行期浪费性能;
  • 必须在运行期动态分发的东西,坚决使用面向对象接口解耦。

第七层:开放---封闭原则的终极境界

学员: 很多书上说开放---封闭原则(OCP)是"对扩展开放,对修改封闭"。但我们在新增无人机业务时,依然要写新的代码、依然要敲新的配置,这算不算修改了代码?

讲师: 这是对 OCP 最常见的教条主义误解。

"对修改封闭"绝不是说整个 Git 仓库不能增加一行代码,而是:

核心骨架与公共基础设施,不因为每次业务扩展而被反复翻出来修改。

新增业务当然需要写新代码:

cpp 复制代码
// 1. 新增无人机检查策略(纯增量文件)
class DroneAirspaceGate { ... };

// 2. 新增无人机任务(纯增量文件)
class DroneDropTask : public ShippingFlow<DroneRequest, DroneResponse, DroneAirspaceGate> { ... };

// 3. 注册挂载(纯增量配置)
TaskDirectory::instance().attach<DroneDropTask>(1004);

但是,整个系统中最核心、最复杂、最容易因改动引发灾难的地方:

  • TaskDirectory(中央注册表)
  • IShippingTask(对外协议抽象)
  • ShippingFlow(公共处理流程骨架)
  • 已有的上门配送、冷链配送的数十个老业务

源码一行都不需要动!编译通过即可保证老逻辑 100% 不受波及。


架构思考自检清单(实战四问)

当你面对一段复杂的业务代码,犹豫是否要使用 OCP 进行重构时,可以用以下四个问题拷问自己:

  1. 变动点到底是什么?
    是外部业务种类的增加(上门、冷链、无人机)?还是内部校验算法、输出渠道、计费规则的增加?
  2. 变动时,你是否总在修改一个稳定的 switch 或长串 if/else?
    如果每次提需求,你都要在一个关键函数里塞进新的分支,说明该处必须抽取扩展点。
  3. 变化是在什么时候决定的?
    • 如果是等用户请求到达、根据报文字段动态判断 ➔ 选择运行时多态(虚接口 + 注册表);
    • 如果在写具体业务类声明时就已经确定了 ➔ 选择编译期多态(模板参数 / Policy 策略)。
  4. 抽象的成本是否值得?
    如果业务只有两个简单分支,且未来一年绝不会增加第三种,那么保持原有的 if/else 是最务实、可读性最高的选择;
    只有当业务具备持续演化、频繁增删子业务的特征时,这套架构才能发挥出"让系统十年不腐烂"的真正威力!
相关推荐
wuminyu2 小时前
C++协程实现接收端的零拷贝Buffer管理原理剖析
java·linux·c语言·jvm·c++
m0_380743872 小时前
C++中的freopen的用法实例详解
开发语言·c++
m0_587383002 小时前
深度解析24小时自助健身房系统开发:从架构设计到落地部署
人工智能·小程序·数据挖掘·系统架构·需求分析
jimy13 小时前
polymorph.cpp里面的“析构函数、智能指针”问题
开发语言·c++
dingdingfish3 小时前
系统架构概述
microsoft·系统架构·ea·architecture
朝朝辞暮i3 小时前
C++ 第 6 课:switch —— 多状态选择
开发语言·c++·算法
朝朝辞暮i4 小时前
C++ 第 4 课:if / else —— 让程序自己做决定
开发语言·c++
苏打豆4 小时前
Pinocchio源码阅读——刚体运动物理量空间变换及求导
c++·线性代数·算法·矩阵·机器人
m0_380743874 小时前
Qt中导航栏实现的详细指南
开发语言·c++