同一批离线样本,有时保留原值,有时裁剪异常值。策略模式把这部分可变化的行为移出使用它的流程。难点不在“多写一个接口”,而在确定替换发生的时机、状态由谁拥有,以及什么契约保持不变。
程序设计专题 · 上一篇:工厂的三种含义 · 下一篇:C++17
1. 从稳定流程中找出变化轴
Context(上下文)负责流程;Strategy(策略)负责一个可替换步骤。本例 Pipeline 接收一个数值并调用过滤规则,Identity 保留原值,Clamp 把它限制到给定区间。
这不要求消灭所有分支。应用仍然可能根据配置选择策略,只是选择发生在装配边界;稳定流程不需要反复理解所有策略细节。如果差异只是一个很短、很稳定的 if,把分支留在函数里往往更直接。
还要说明可替换契约:本例处理有限 double,不修改输入外部状态,输出仍是同一单位的数值。不处理 NaN、传感器故障、时间同步或线程并发,也不是可直接部署的实时机器人控制器。
2. 运行时多态:对象拥有当前策略
适合需要在运行时选择不同策略类型、每种策略有自己配置或状态的场景。Context 用 unique_ptr 独占策略,基类虚析构使通过接口释放对象正确。
下载完整源码
#include <algorithm> #include <iostream> #include <memory> #include <stdexcept> #include <utility>
struct Filter { virtual ~Filter() = default; virtual double apply(double value) const = 0; }; struct Identity final : Filter { double apply(double value) const override { return value; } }; class Clamp final : public Filter { double low_, high_; public: Clamp(double low, double high) : low_(low), high_(high) { if (!(low <= high)) throw std::invalid_argument("invalid bounds"); } double apply(double value) const override { return std::clamp(value, low_, high_); } }; class Pipeline { std::unique_ptr<Filter> filter_; public: explicit Pipeline(std::unique_ptr<Filter> filter) { set_filter(std::move(filter)); } void set_filter(std::unique_ptr<Filter> next) { if (!next) throw std::invalid_argument("filter is required"); filter_ = std::move(next); } double process(double value) const { return filter_->apply(value); } }; int main() { Pipeline pipeline(std::make_unique<Identity>()); std::cout << "identity=" << pipeline.process(12) << '\n'; pipeline.set_filter(std::make_unique<Clamp>(0, 10)); std::cout << "clamped=" << pipeline.process(12) << '\n'; try { pipeline.set_filter(nullptr); } catch (const std::invalid_argument& error) { std::cout << error.what() << '\n'; } std::cout << "still=" << pipeline.process(12) << '\n'; }
|
g++ -std=c++17 -Wall -Wextra -Werror -pedantic strategy_runtime.cpp -o strategy_runtime ./strategy_runtime
|
输出:
identity=12 clamped=10 filter is required still=10
|
set_filter() 先验证新对象,再替换成员。因此这个特定的“拒绝空策略”失败路径不会丢掉当前策略,最后仍得到 10。它并不意味着任意有副作用的策略执行都能自动回滚。
Clamp 在构造时验证上下界顺序,满足 std::clamp 的前提;数值输入仍需遵守前面限定的有限值契约。外部数据校验应放在输入边界,而不是把演示代码当成完整控制保护逻辑。
创建新策略由调用者完成,可以直接 make_unique,也可以交给上一篇的简单工厂。策略接口本身无需了解配置文件格式。
3. 可调用对象与模板策略:不一定要建立类层级
单个行为签名清楚时,可以使用函数对象或 lambda。std::function<double(double)> 对不同可调用类型做类型擦除,因而同一个成员可在运行时换成另一种可调用对象;StaticPipeline<Filter> 则把策略类型固定为模板参数。
下载完整源码
#include <algorithm> #include <functional> #include <iostream> #include <stdexcept> #include <utility>
class CallablePipeline { std::function<double(double)> filter_; public: explicit CallablePipeline(std::function<double(double)> filter) { set_filter(std::move(filter)); } void set_filter(std::function<double(double)> next) { if (!next) throw std::invalid_argument("filter is required"); filter_ = std::move(next); } double process(double value) const { return filter_(value); } };
template<class Filter> struct StaticPipeline { Filter filter; double process(double value) const { return filter(value); } }; struct Twice { double operator()(double value) const { return value * 2; } }; int main() { const double limit = 10; CallablePipeline dynamic([limit](double value) { return std::clamp(value, 0.0, limit); }); std::cout << "callable=" << dynamic.process(12) << '\n'; dynamic.set_filter(Twice{}); std::cout << "replaced=" << dynamic.process(12) << '\n'; StaticPipeline<Twice> fixed{Twice{}}; std::cout << "static=" << fixed.process(12) << '\n'; try { dynamic.set_filter({}); } catch (const std::invalid_argument& error) { std::cout << error.what() << '\n'; } }
|
g++ -std=c++17 -Wall -Wextra -Werror -pedantic strategy_values.cpp -o strategy_values ./strategy_values
|
输出:
callable=10 replaced=24 static=24 filter is required
|
lambda 按值捕获 limit,不会借用一个已经离开作用域的局部变量。把 [limit] 机械地改为 [&] 再长期保存,是常见的悬垂引用来源。若确实捕获指针或引用,需要另外保证被引用对象的寿命和同步。
模板版本的 StaticPipeline<Twice> 只能保存 Twice 类型的策略。它可以更改同类型策略的状态,但不能在运行时随意塞入一个不相关的 lambda 类型;换模板参数得到的是另一种 Context 类型。不要把编译期可替换和运行时可替换混称为一种能力。
4. 三种表达的成本与限制
| 表达 |
选择时机 |
所有权/限制 |
适合起点 |
虚接口 + unique_ptr |
运行时 |
本例独占且可空,所以要验证;通常涉及动态分配 |
有状态对象、稳定接口、模块边界 |
std::function |
运行时 |
C++11;目标需可复制,可能分配;空调用抛 bad_function_call |
单一签名的回调或局部算法 |
| 模板 + 函数对象 |
编译期固定类型 |
无须类型擦除;实例化可能增加编译时间和代码体积 |
热路径且具体类型可在编译期确定 |
不能仅凭写法断言“模板一定快”或“虚调用一定慢”。编译器可能内联、去虚化,也可能因代码体积影响指令缓存。应测量真实数据、调用频率和构建选项;一个对已知具体对象的循环不能自动代表所有插件边界的性能。
std::function 不能直接保存一个捕获 unique_ptr 的不可复制 lambda。C++23 的 std::move_only_function 可以表达这一需求,见 05。它不是 std::function 的无条件替代品:接口若要求复制,换成只能移动就会改变契约。
5. Strategy 与相邻模式的边界
- Factory 关心创建哪个对象;Strategy 关心对象之后怎样完成某个步骤。一个可以没有另一个。
- State 关心对象状态与合法迁移,行为随内部状态变化;Strategy 通常由外部选择算法。结构相似,动机不同。
- Template Method 通常让基类定义流程、派生类覆盖部分步骤;Strategy 通过组合把步骤交给另一对象。
- Decorator 在现有行为外叠加职责;它不只是从多个备选行为里挑一个。
共享一个策略对象也不等于线程安全。即使指针替换有锁,策略内部的可变状态仍需自己的同步或明确单线程约束。本例只有单线程、无状态算法;移动后的 Context 不参与后续调用,若工程中需要支持该状态应明确接口约定。
6. 阅读历史笔记时保留哪些判断
原策略模式笔记保留了更多扩展示意。本篇只把两个完整程序作为验证对象,不把历史对象池、事务回滚或性能数字当作已验证结论。首先确认寿命、错误传播和状态边界,再决定是否需要池化、并发或更多模式组合。
参考
C++ Core Guidelines F.53讨论非局部使用 lambda 时的引用捕获风险;C++17 草案 N4659 的 [func.wrap.func]、[unique.ptr] 与 [alg.min.max] 可核对 std::function、独占指针和 clamp 的契约。代码与运行输出是本篇自己的可复现示例。