1. 为什么说C里的策略模式比GoF书上的更分裂1.1 GoF策略模式在C中的教科书写法当年我第一次接触策略模式是从《设计模式》那本GoF书开始的。书里的结构非常简单一个抽象策略基类一堆具体策略子类再加一个持有策略引用的上下文类。放到C里绝大多数教程给出的代码长这样class ICompression { public: virtual ~ICompression() default; virtual void compress(const std::string input, std::vectoruint8_t output) 0; }; class ZipCompression : public ICompression { public: void compress(const std::string input, std::vectoruint8_t output) override { // zip实现 } }; class Lz4Compression : public ICompression { public: void compress(const std::string input, std::vectoruint8_t output) override { // lz4实现 } }; class DataExporter { public: explicit DataExporter(std::unique_ptrICompression compression) : compression_(std::move(compression)) {} void exportData(const std::string data) { std::vectoruint8_t compressed; compression_-compress(data, compressed); // 写文件... } private: std::unique_ptrICompression compression_; };这个写法本身没错它把压缩算法这个变化点从导出逻辑里抽离了出来新增一种压缩算法时不用改DataExporter的代码。但如果你在C项目里实际这么干过一段时间你一定会隐隐觉得哪里不对劲每次调用策略方法都要走一次虚函数跳转策略对象通常得在堆上分配面对多个变化维度时类的数量会迅速失控。这就是C里策略模式最容易被误解的地方。很多人以为策略模式就等于抽象基类加虚函数但C同时提供了另一条路线模板。这两种路线在分发时机、性能特征、代码组织方式上完全不同。所以我更愿意说C里的策略模式是分裂的它分裂成运行期多态和编译期多态两个世界。1.2 继承式策略的硬伤虚函数成本与类型强耦合继承式策略第一个绕不开的问题就是虚函数调用的代价。虚函数调用本身只是一次间接跳转加一点缓存压力单次调用消耗并不吓人。真正的问题在于编译器无法跨过虚函数边界做内联这意味着每次策略调度都无法享受到内联带来的优化。在一个低延迟模块里比如游戏引擎的每帧逻辑或者嵌入式设备的中断处理路径策略被调用的频率可能高达每秒数十万次这时候虚函数调用的成本就不再是可以忽略的开销。我参与过一个网络转发模块的优化业务逻辑里有一个是否压缩的策略接口只是判断数据大小决定要不要压缩。最初用虚函数实现压测时发现这个判断路径占了大约6%的CPU时间。后来改成了模板和if constexpr做编译期分发把判断逻辑内联到调用点这6%直接降到了千分之一以下。对于热路径上的策略是否走虚函数差别是数量级的。第二个硬伤是类型强耦合。继承式策略要求所有策略都从同一个抽象基类派生这个基类定了死所有策略必须暴露一套相同的接口签名。但真实业务里不同策略需要不同的初始化参数、不同的返回信息、不同的生命周期约束。硬塞进同一个接口之后你只能往接口里加可选参数、加默认实现、加一堆纯虚函数不管使用的派生类接口越来越胖抽象慢慢就失去了意义。更难受的是多维度组合。假设一个上下文同时存在三个策略维度压缩算法、序列化格式、加密算法每个维度三种实现。继承式策略下你可能需要维护3×3×3个类或者大量的组合逻辑而无论怎么组合策略维度之间一旦互相影响基类接口就会变得无比臃肿。这类问题在C里其实有更好的解法也就是编译期的Policy-Based Design。1.3 C策略模式的真正分野运行期多态与编译期多态在C里谈策略模式进阶第一件事就是建立一个认知框架策略选择发生在运行期还是编译期决定了整个设计形态。运行期多态适用于策略会随用户输入、配置文件、插件机制动态变化的情况。典型例子是导出工具的压缩算法用户在下拉框里选zip还是lz4程序运行时根据选择构造对应的策略对象。这时候继承加虚函数也好std::function加lambda也好本质都是建立一张运行时的分发表。编译期多态适用于策略在编译时就能确定的情况。典型例子是std::sort的比较器排序代码写死的时候就知道要用升序还是降序不需要用户运行时再选。通过模板传入比较器编译器在实例化时直接把比较逻辑内联进排序循环既保留了扩展能力又不损失性能。进阶的关键不是二选一而是在同一个系统里根据场景混用。我在实际项目里的经验是凡是策略选择由外部配置或用户在运行时决定用运行期方案凡是策略选择由编译期已知的类型特性决定用编译期方案。判断错了就很容易写出一个性能崩溃的纯虚接口或者一个完全无法热切换的模板泥潭。2. 模板与基于策略的设计把决策压进编译期2.1 模板策略入门从sort比较器说起C里最经典的编译期策略示例几乎人人都见过就是给std::sort传比较器。很多人没意识到这其实就是一个完整的策略模式struct AscendingOrder { bool operator()(int a, int b) const { return a b; } }; struct DescendingOrder { bool operator()(int a, int b) const { return a b; } }; template typename SortStrategy void processAndSort(std::vectorint data, SortStrategy strategy) { // 模拟一些前置处理 for (auto v : data) { v normalize(v); } std::sort(data.begin(), data.end(), strategy); } // 使用 std::vectorint data {4, 1, 3, 2}; processAndSort(data, AscendingOrder{});这个模式里SortStrategy就是一个策略processAndSort是上下文std::sort的调用是策略的执行点。对比继承式方案模板策略没有虚函数AscendingOrder::operator()会被直接内联到sort的实例化代码里。std::sort本身为标准库容器做了针对性的性能优化配合内联的比较器在数据量稍大时比传函数指针和虚函数都要快不少。如果你之前只是机械使用std::sort的比较器参数现在可以把它升维理解算法库要求使用者提供的每一个可调用对象本质上都是一个模板策略参数。包括std::accumulate的二元操作、std::unique的相等比较器、std::transform的一元变换函数它们和面向对象味道很浓的策略模式没有任何本质区别。2.2 基于策略的设计allocator与多参数策略组合真正把模板策略推到极致的是《Modern C Design》里提出的Policy-Based Design。它的核心思想是把类模板的参数从一个类型升级成一组策略类型通过正交组合让一个主类获得多种行为维度。我觉得理解这个概念最容易切入的例子就是标准库容器里的分配器。一个std::vectorT, Allocator这里的Allocator就是一个内存分配策略。同一个vector逻辑可以配合std::allocator使用常规堆内存配合池化分配器减少小对象碎片配合共享内存分配器做跨进程访问。vector的实现代码只有一份但通过策略参数它能在编译期获得完全不同的内存行为。比标准库更彻底的是自己定义一个多策略引擎。假设你在写一个小型游戏引擎的调度模块线程模型、日志方式、内存来源这几个维度都可能变化struct SingleThreaded { void lock() {} void unlock() {} }; struct MultiThreaded { void lock() { mutex_.lock(); } void unlock() { mutex_.unlock(); } std::mutex mutex_; }; struct NullLogging { void log(const std::string) {} }; struct FileLogging { void log(const std::string msg) { // 写文件实现 } }; template typename ThreadingPolicy SingleThreaded, typename LoggingPolicy NullLogging class GameTaskQueue : public ThreadingPolicy, public LoggingPolicy { public: void pushTask(const std::string task) { ThreadingPolicy::lock(); LoggingPolicy::log(push task); tasks_.push_back(task); ThreadingPolicy::unlock(); } // ... private: std::vectorstd::string tasks_; };这里有两个关键点。第一GameTaskQueue继承自策略类而不是把它们作为成员这是有意为之如果某个策略是空类继承可以让它参与空基类优化不增加任何对象体积。第二调用方在编译期自由组合GameTaskQueueSingleThreaded, NullLogging singleQueue; GameTaskQueueMultiThreaded, FileLogging threadSafeQueue;这种组合能力如果换成继承式策略模式需要为每一种组合写一个派生类而Policy-Based Design不需要任何额外代码。策略维度增加时继承式方案的问题会指数放大策略模板方案只是多一个模板参数而已。2.3 CRTP变体静态分发但保持接口约束模板策略的缺点是接口是隐式的。只要一个类型具备operator()或者某个名字相同的方法它就能被当作策略使用。类型检查宽松是好事但团队成员写错方法名时编译器会报出一长串模板实例化错误可读性很差。CRTP提供了一种折中用基类模板给派生策略一个显式的骨架同时仍然是编译期分发。template typename Derived struct BaseCompressionStrategy { void compress(const std::string input, std::vectoruint8_t output) { // 可以做参数校验、统计耗时等公共逻辑 static_castDerived*(this)-compress_impl(input, output); } }; struct ZipCompression : BaseCompressionStrategyZipCompression { void compress_impl(const std::string input, std::vectoruint8_t output) { // zip具体实现 } }; struct Lz4Compression : BaseCompressionStrategyLz4Compression { void compress_impl(const std::string input, std::vectoruint8_t output) { // lz4具体实现 } }; template typename CompressionPolicy void exportWithCompression(const std::string data, CompressionPolicy comp) { std::vectoruint8_t output; comp.compress(data, output); // ... }CRTP的优点在于公共逻辑校验、监控、日志被放进了非虚的基类函数里调用方仍然通过统一的compress接口操作不同策略而每次调用都会在编译期被静态分发到具体策略的compress_impl上没有虚函数开销。代价是你拿到的具体策略类型各不相同把它们放进同一个容器或者运行时切换就做不到了。CRTP适合那些编译期已确定策略形态、但希望复用公共骨架的场景它是从模板策略到运行期策略之间的中间站。3. std::function与lambda运行期策略的轻量化改造3.1 从新类型到新函数策略表现形式的迁移很多C项目里策略的数量不多、逻辑也不复杂但确实需要在运行期切换。如果严格按GoF写一个抽象基类再写几个派生类文件维护成本其实不小。这种情况下我会直接用std::function加lambda让策略从一个类型降级成一个可调用对象。class DataExporter { public: using CompressStrategy std::functionvoid(const std::string, std::vectoruint8_t); explicit DataExporter(CompressStrategy strategy) : strategy_(std::move(strategy)) {} void exportData(const std::string data) { std::vectoruint8_t compressed; strategy_(data, compressed); // 写文件... } private: CompressStrategy strategy_; }; // 用户侧 DataExporter exporter([](const std::string input, std::vectoruint8_t output) { // 写一个轻量zip逻辑或者调用第三方库 }); DataExporter noneExporter([](const std::string input, std::vectoruint8_t output) { output.assign(input.begin(), input.end()); });这种写法不再需要为每个策略定义一个类。lambda天然支持捕获环境状态一个策略需要配置参数时直接在捕获列表里带上就行。比如压缩级别、加密密钥、目标文件路径这些运行期数据不需要塞进策略类的构造函数而是通过lambda捕获闭包。对比继承式策略省掉了类声明、构造函数、虚函数重写一大堆模板代码。事件驱动系统里这种写法尤其顺手。UI层选了一个菜单项对应的处理逻辑可能只是一个捕获了界面状态的lambda服务端路由里HTTP路径对应的处理器也能用std::function存储请求来了直接查表调用。核心语义没有变依然是把变化的算法封装起来、让调用方选择但代码形状轻盈得多。3.2 生命周期陷阱与性能损耗度量std::function方案虽然轻但有两个风险点必须重视生命周期和性能。生命周期问题最容易踩。lambda捕获了一个栈对象的引用而这个策略被储存到容器里栈对象提前销毁后策略调用就是悬空引用。举一个真实例子我曾经见过一段代码在循环里构造局部配置对象然后把捕获它的lambda注册到全局策略表中循环结束配置对象析构下次调用策略时直接未定义行为。这种情况在Debug下可能表现为数据错乱在Release下才偶发崩溃非常难排查。防御手段有三个优先以值捕获和策略生命周期相当的数据如果必须捕获引用一定要用shared_ptr管理被捕获对象的生命周期或者把策略注册表做成显式作用域保证注册、调用、反注册都在一个生命周期范围内。性能方面std::function调用本质是类型擦除加一次间接跳转和虚函数的调用成本处于同一量级或者略高。某些实现里std::function在构造时可能发生堆分配尤其是捕获了大量数据的lambda在高频调用的热路径上这种分配会带来明显的开销。我的判断标准是如果策略在每个渲染帧、每次网络收包、每次按键事件里都要执行先别用std::function考虑模板方案如果策略是按用户操作、按文件导入这类低频路径调用std::function带来的便利就远大于这点成本。3.3 用容器管理策略族std::function还有个大优势就是它天生可以作为容器的元素类型我们可以用映射表集中管理一组策略。这种注册表式策略管理在实现配置驱动的功能时非常实用。class CompressionManager { public: using CompressFn std::functionstd::vectoruint8_t(const std::string); void registerStrategy(const std::string name, CompressFn fn) { strategies_[name] std::move(fn); } std::vectoruint8_t compress(const std::string name, const std::string input) const { auto it strategies_.find(name); if (it strategies_.end()) { throw std::runtime_error(unknown strategy: name); } return it-second(input); } private: std::unordered_mapstd::string, CompressFn strategies_; }; CompressionManager manager; manager.registerStrategy(none, [](const std::string s) { return std::vectoruint8_t(s.begin(), s.end()); }); manager.registerStrategy(reverse, [](const std::string s) { std::vectoruint8_t out(s.rbegin(), s.rend()); return out; }); // 运行期根据配置选策略 auto chosen config.get(compression); auto result manager.compress(chosen, payload);这种写法和内存中的插件系统非常接近。新增一种策略时只需要在初始化阶段register一个lambda不用改调用方代码也不用新增类文件。策略表还可以从配置文件批量构建把字符串名称和工厂函数绑定在一起。我之前维护过一个消息处理器最开始是十几个if-else判断消息类型后来改成这张策略注册表每次加新协议只需要新增一个handler文件并在初始化时注册一次代码清爽了很多调试时也能直接在表里看到所有支持的策略。当然注册表方案不适合处理参数差异特别大的策略。每个策略如果都需要完全不同的入参结构std::function的统一签名反而成为束缚此时回到接口加void*或variant参数的老路不如断言每个策略单独定义一个类来得清晰。4. C20 concepts让模板策略也有体检报告4.1 concepts约束策略接口的基本姿势模板策略最大的痛点在于接口是隐式的。在C11到C17的年代一个策略概念上应该提供compress方法但编译器不会提前检查。你写错了方法名或者参数类型对不上错误信息要等模板实例化时才会爆发而且常常指向几十行以外的STL内部头文件。C20的concepts改变了这一点它让模板策略也能获得类似于抽象基类的接口声明能力。template typename Strategy concept CompressionStrategy requires( Strategy strategy, const std::string input, std::vectoruint8_t output) { { strategy.compress(input, output) } - std::same_asvoid; };这个concept定义了一个合法的压缩策略类型必须能在给定input和output参数的情况下调用compress且返回值必须是void。它不要求策略继承任何基类也不限定具体的类型只要满足接口形状即可。有了它上下文模板可以这样写template CompressionStrategy S void runCompression(S strategy, const std::string input, std::vectoruint8_t output) { strategy.compress(input, output); }当调用方传入一个不满足约束的类型时编译器给出的错误提示会直接说约束未满足而不是抛出一长串和用户代码毫无关系的实例化日志。在一个多人的大项目里这种错误信息的改善带来的效率提升是肉眼可见的。4.2 模板策略的错误信息革命没有concepts的年代写错策略接口的报错能让人崩溃。想象一个模板函数在20层实例化之后发现某处缺少一个成员函数编译器打印出来的错误可能是几千行的模板内部上下文每个候选模板都要展开一次。你需要在里面翻半天才能找到自己传入的类到底错在哪。concepts的价值不是增加了一种语法而是把策略接口这个设计意图直接表达了出来。这有点像拿合同和口头约定做对比继承式策略有明文的接口合同抽象基类模板策略以前是口头约定方法名叫compress就行concepts把口头约定变成了可以自动检查的合同。成员用错方法名、漏掉某个参数、返回值类型不对编译器在模板函数的入口处就能拦截直接报出你这个类型不满足CompressionStrategy约束。我在一个图形算法库里实践过这种方式。库里有多种像素格式每种格式对应一个处理策略以前用模板写遇到用户传错类型时邮件列表里全是抱怨。加了concepts之后报错信息基本都能精准定位到用户自己的代码行技术支持成本明显下降。如果你正在维护一个提供给团队外部使用的模板库concepts不是可选优化而是必须。4.3 requires表达式与策略的可选能力检测concepts更进一步的能力是检测策略的可选能力。某些策略支持flush操作某些不支持某些策略提供reset某些策略没有。在继承式策略里这种情况通常会导致基类加一个默认空实现的虚函数或者用dynamic_cast做能力查询。在模板策略里可以用requires表达式加if constexpr优雅得多。template typename Strategy void callWithFlush(Strategy strategy) { // 处理数据... if constexpr (requires { strategy.flush(); }) { strategy.flush(); } else { // 没有flush能力的策略做兜底处理 } }这里的逻辑是如果传入的策略类型存在一个无参的flush成员函数就在数据处理后调用它如果不存在则走兜底逻辑。两个分支都在编译期确定不会带来运行期开销。这种能力组合用继承式策略很难做干净给基类加上flush虚函数所有不支持的策略都得空实现不加调用方就没法统一接口。编译期检测直接把支持与否变成了类型本身的属性。还有一个实用的组合concepts定义核心接口requires表达式检测可选接口两者配合可以构建一个既严格又灵活的策略约束体系。核心接口保证策略一定能被使用可选能力让策略之间可以存在合理差异而不会被迫塞进同一个庞大的基类里。5. 两个实战场景游戏背包排序与嵌入式通信协议解析5.1 场景一C小游戏里的背包物品排序策略用策略模式最顺手也最好理解的项目我认为是游戏背包物品排序。做小游戏时背包里的物品包含名称、品质、类型、数量多个属性玩家希望按不同维度排序。如果一开始就写if-else随着排序维度增加函数会越来越难维护。struct Item { std::string name; int quality 0; // 品质数值越大越好 int type 0; // 道具类型武器、防具、消耗品等 int count 0; }; // 排序策略函数对象或lambda auto byQualityThenName [](const Item a, const Item b) { if (a.quality ! b.quality) return a.quality b.quality; return a.name b.name; }; auto byTypeThenQuality [](const Item a, const Item b) { if (a.type ! b.type) return a.type b.type; return a.quality b.quality; }; auto byCountAsc [](const Item a, const Item b) { return a.count b.count; }; class Inventory { public: void setSortStrategy(std::functionbool(const Item, const Item) sorter) { sorter_ std::move(sorter); } void sort() { if (sorter_) { std::sort(items_.begin(), items_.end(), sorter_); } } void addItem(const Item item) { items_.push_back(item); } private: std::vectorItem items_; std::functionbool(const Item, const Item) sorter_; };玩家在界面上点击按品质排序、按类型排序时只需要调用setSortStrategy并传入对应的lambda然后触发sort方法。这就是典型的运行期策略切换策略由玩家的操作在运行期决定策略本身是轻量的比较函数。为什么这里不用模板策略因为玩家操作永远发生在运行期程序在编译期不知道玩家会点哪个按钮。如果强行模板化就得为每一种排序方式编译一个Inventory实例还得维护一张函数指针表得不偿失。但为什么不用传统抽象基类呢因为比较逻辑只是两个Item之间的一个布尔表达式为它写一个类加一个虚函数代码量翻倍却没带来任何额外好处。std::function加lambda正好落在合理的中间位置。5.2 场景二嵌入式环境下的协议解析策略嵌入式Linux应用开发里策略模式同样常见但对性能和内存的限制更严格。我做过一个传感器数据采集程序需要从多个型号的传感器接收数据帧每种传感器的帧格式不同帧头不同、有效数据长度不同、字节序不同。协议解析逻辑天然可以用策略模式建模。嵌入式场景下我不会首选std::function因为这种设备上堆分配和类型擦除带来的开销有风险。更合适的方案是模板加concepts把协议解析策略在编译期绑定template typename PayloadPolicy concept FrameParser requires(PayloadPolicy parser, const uint8_t* data, size_t len, ParsedFrame frame) { { parser.parse(data, len, frame) } - std::same_asbool; }; template FrameParser Parser class SensorReader { public: explicit SensorReader(Parser parser) : parser_(parser) {} bool readAndHandle(const uint8_t* buffer, size_t len) { ParsedFrame frame; if (!parser_.parse(buffer, len, frame)) { return false; } handleFrame(frame); return true; } private: Parser parser_; }; struct SensorAProtocol { bool parse(const uint8_t* data, size_t len, ParsedFrame frame) { if (len 16) return false; // 传感器A的帧格式解析 return true; } }; struct SensorBProtocol { bool parse(const uint8_t* data, size_t len, ParsedFrame frame) { if (len 24) return false; // 传感器B的帧格式解析 return true; } };这里用模板策略是因为每个传感器型号在编译时就是已知的固定类型。编译期分发让parse调用可以被内联整个解析过程不涉及虚函数、不涉及堆分配对于中断上下文或实时性要求较高的采集路径非常合适。换一种传感器型号时改的是模板参数而不是运行时的分派逻辑。约束方面用concept检查parse签名防止团队成员写漏参数。嵌入式场景还有一层考虑代码体积。模板策略可能会为每种传感器实例化一份SensorReader代码如果型号特别多需要关注代码膨胀。我的经验是控制模板维度数量并且把公共逻辑提取到非模板基类里形态就接近于前面说的CRTP方案。这样既能保证内联收益又能把重复代码压到最小。5.3 游戏场景与嵌入式场景的选型对照这两个场景放在一起非常能说明问题。同样的策略模式进阶在游戏背包里策略在运行期变化、数据结构复杂、调用频率低在嵌入式协议解析里策略在编译期确定、调用频率高、资源受限。选型结果完全不同。实现方式分发时机性能特征灵活性资源占用适合场景虚函数继承运行期有间接跳转难以内联很高可热替换需堆分配对象插件、配置驱动、低频业务逻辑std::function加lambda运行期类型擦除间接调用很高可热替换可能堆分配内部存储事件回调、策略注册表、UI逻辑模板加concepts编译期可完全内联零成本抽象低类型固定几乎无额外开销高频路径、算法库、资源受限设备CRTP加模板基类编译期可完全内联复用公共逻辑低类型固定几乎无额外开销需要公共前处理的后处理策略族我给自己定的选型顺序是默认写模板加concepts能编译期决定就编译期决定确实要运行期切换时优先用std::function因为它简单直接只有当策略逻辑足够复杂、参数形态差异大、需要独立维护时才去写抽象基类和派生类。这个顺序在多数项目里都成立也是我个人从设计模式原教旨主义转向实用C后的最大变化。6. 策略模式在C里的反面清单我踩过的坑6.1 过度策略化一个函数被拆成五个文件策略模式最容易被滥用。我曾经在一个内部工具项目里看到一段代码一个总共只有几十行的处理流程被拆出了三层策略接口压缩策略、存储策略、上报策略。每种策略只有一个实现而且没有任何迹象表明近期会出现第二种实现。代码的调用链变得极长读代码的人要在五个文件之间来回跳才能拼出完整逻辑。这种过度设计的根源是把未来的扩展性当成了当下的需求。策略模式的价值是消除重复、隔离变化但它自身也带来了间接层。间接层不只是运行时的函数调用更是人读代码时的认知负担。判断标准很简单当前是否至少有两个真实存在的策略实现需要互换如果没有就没有理由引入策略模式。一个实现时先写死等第二个实现真来了再做抽象一点也不晚。6.2 策略对象的生命周期与所有权混乱策略模式在C里有一个逃不掉的议题谁拥有策略对象继承式策略下上下文通常会持有一个指向策略基类的指针或引用。调用方把策略new出来传进去然后上下文可能提前销毁、可能在后排延迟销毁、可能在多个线程间共享所有权一旦模糊内存问题立刻出现。我见过最典型的问题是用裸指针传入一个局部策略对象上下文把它保存下来以备后续使用结果局部对象出了作用域就被析构上下文持有的指针变成悬空指针。另一种相反的情况是用shared_ptr处处传递结果策略对象生命周期无限延长直到程序退出才释放内存峰值居高不下。我的建议是优先把策略对象以值的方式存进上下文尤其是std::function或模板策略很容易做到值语义必须用指针时明确约定所有权是独占还是共享凡是跨模块边界传递策略对象绝不用裸指针用unique_ptr表达独占所有权用shared_ptr表达共享所有权。所有权约定应该写进接口注释因为这是运行时错误的高发区。6.3 并发环境下策略共享的问题团队用了策略模式后常常会出现一个隐藏问题多个线程共享同一个策略对象。对于无状态策略也就是operator()里不修改任何成员变量、不依赖内部标志位共享是安全的。这类策略对应的是纯函数只要输入相同输出一定相同。问题出在那些有状态的策略上。我遇到过一种计数策略每次执行都会把一个内部计数器加一用于统计调用频次。单线程时代码正常进入多线程后计数器出现数据竞争偶尔丢计数甚至导致整个调用链的缓存频繁失效。更隐蔽的是某些策略内部为了性能加入了缓存缓存本身是可变状态共享之后各种诡异问题接踵而至。处理方式有两个方向。一是让策略对象变成真正无状态的把统计、缓存这些状态抽出来由调用方通过参数传入二是每个线程持有一个自己的策略实例从所有线程共享一个策略改成每个线程独立构造策略。这两种方向选哪个取决于策略本身的语义但一定要在设计阶段就想清楚而不是等并发测试挂了再补救。6.4 什么时候直接if-else反而更好说了这么多进阶技巧最后必须泼一盆冷水并不是所有变化点都值得上策略模式。一个支付系统当前只有支付宝和微信两种方式而且业务代码里只是根据渠道枚举走两个分支那直接用if-else或者switch反而是最清晰的做法。渠道少、逻辑简单、变化频率低引入策略类会让简单的收支逻辑散落在多个文件里。有人会说将来要加第三种渠道啊。但将来可能加不等于现在必须设计。等第三种渠道真的出现了再按前文的注册表方式把策略引进来改动成本不比一开始就设计更高因为业务逻辑的变化点你已经知道了。提前引入策略模式等于为一个还不存在的需求付出了当下的复杂度和维护成本。我个人实践中的体会是策略模式适合那种变化点清晰、策略之间差异较大、替换行为有意义的场景。如果变化点不出来或者所谓的变化只是不同参数组合直接写参数化的普通代码或if-else你节省的时间和避免的问题一定比套用途多。写代码最难得不是用上高级技巧而是知道什么时候不该用。最后分享一个我自己的小习惯每次想给某块逻辑引入策略模式之前先写一个最小可运行的原始版本用最简单的分支实现然后把策略数量和策略切换频率这两个数字写出来。如果策略数量小于等于2切换频率低到可以重启换配置就不要做任何策略抽象如果策略数量开始增长到3个以上或者同一个系统里确实出现了多处类似的分支逻辑再动手重构。这个办法帮我挡掉了大量没必要的抽象也让我真正需要策略模式的时候每次都格外果断。
