前言:从"外循环分发"到"内循环编排"
在上篇 中,我们通过**"纯虚接口 + 泛型工厂 + 注册目录"的三位一体架构,彻底消灭了业务分发中心 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)",需要检查海关清关资质:
- 只需要写一个独立的
CustomsGate策略类; - 继承
ShippingFlow<..., CustomsGate>; - 公共流程
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_);
只要你的策略类满足以下契约:
- 能够无参构造;
- 拥有名为
verify的公共成员函数; - 能接收
SessionInfo&参数,且返回值能转为int。
它就能通过编译!
带来两大绝顶好处:
-
零虚函数开销,极致内联优化 :
因为没有虚表指针(vptr),编译器可以在编译期直接将gate.verify()**完整内联(Inline)**展开到执行流程中,性能损耗为 0! -
编译期严格截断错误 :
如果你写了一个非法的 Gate 类(比如方法名写成了check()而不是verify()),或者返回值不匹配:cppclass 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 进行重构时,可以用以下四个问题拷问自己:
- 变动点到底是什么?
是外部业务种类的增加(上门、冷链、无人机)?还是内部校验算法、输出渠道、计费规则的增加? - 变动时,你是否总在修改一个稳定的
switch或长串if/else?
如果每次提需求,你都要在一个关键函数里塞进新的分支,说明该处必须抽取扩展点。 - 变化是在什么时候决定的?
- 如果是等用户请求到达、根据报文字段动态判断 ➔ 选择运行时多态(虚接口 + 注册表);
- 如果在写具体业务类声明时就已经确定了 ➔ 选择编译期多态(模板参数 / Policy 策略)。
- 抽象的成本是否值得?
如果业务只有两个简单分支,且未来一年绝不会增加第三种,那么保持原有的if/else是最务实、可读性最高的选择;
只有当业务具备持续演化、频繁增删子业务的特征时,这套架构才能发挥出"让系统十年不腐烂"的真正威力!