策略模式(Strategy Pattern)
从"为什么需要它"到"底层是怎么跑起来的",一篇讲透行为型设计模式中最优雅的一个。
目录
- 文档定位
- 引子:一堆if-else引发的灾难
- 专业定义
- 策略模式的结构
- 从零实现:一步步重构
- 底层实现原理
- 现代C++下的策略模式
- 策略模式 vs 相似模式
- 真实工程案例
- 优缺点与适用场景
- 常见误区
- 面试速记卡
文档定位
本文面向两类读者:
- 初学者:第一次接触设计模式,希望通过本文彻底搞懂"策略模式解决了什么问题,怎么写,和普通的函数参数传递有什么本质区别"。
- 有一定经验的开发者:已经会用策略模式,但希望理解它在对象内存布局、虚函数分发、以及现代 C++(
std::function、模板、std::variant)中的多种实现方式与性能取舍。
阅读完本文,你应该能够:
- 清晰说出策略模式解决的核心矛盾——算法的多变性 与 使用算法的代码的稳定性 之间的冲突;
- 用 C++ 从"面向对象继承版"到"函数对象版"再到"模板静态分发版"完整实现一遍策略模式,理解每种写法的性能与灵活性权衡;
- 知道策略模式在真实系统(比如 ASR 解码策略切换、排序/比较器、损失函数选择)中的具体应用;
- 面试时能讲清楚"策略模式和简单的if-else到底差在哪",而不是止步于背 UML 图。
引子:一堆if-else引发的灾难
假设你在写一个语音识别系统的解码模块,支持三种解码策略:贪心搜索(Greedy Search)、束搜索(Beam Search)、加权有限状态转换器搜索(WFST Search)。最朴素的写法:
std::stringDecode(conststd::vector<float>&logits,conststd::string&method){if(method=="greedy"){// 贪心解码逻辑,几十行代码// ...return"greedy_result";}elseif(method=="beam"){// 束搜索逻辑,上百行代码// ...return"beam_result";}elseif(method=="wfst"){// WFST解码逻辑,涉及图搜索,几百行代码// ...return"wfst_result";}throwstd::invalid_argument("unknown decode method");}这段代码短期能跑,但随着项目发展,问题会逐渐暴露:
- 函数体积失控:三种算法的实现代码全部堆在一个函数里,少则几百行,多则上千行,可读性和可维护性直线下降;
- 违反单一职责原则:
Decode函数同时承担了"贪心怎么搜"、“束搜索怎么搜”、"WFST怎么搜"三件事,任何一种算法的 bug 修复都要在这个巨大的函数里定位; - 违反开闭原则:新增一种解码策略(比如后来又要支持 RNN-T 专用解码),必须回到这个函数里再加一个
else if分支,原有代码被迫再次修改,哪怕它已经上线测试稳定; - 难以单独测试:想单独给"束搜索"这一个算法写单元测试,却因为它和其他算法耦合在同一个函数体内,无法独立编译/独立验证;
- 难以运行时动态切换:如果要在程序运行过程中根据置信度反馈动态切换解码策略,
if-else版本很难优雅地支持"策略对象可以被替换"这种需求。
策略模式的本质,就是把"一个问题的多种可互换算法/行为",各自封装成独立的、遵循同一接口的对象,让使用方在运行时可以自由切换,而不需要用if-else或switch把所有算法的实现硬编码在一起。
专业定义
策略模式(Strategy Pattern)是一种行为型设计模式(Behavioral Pattern),出自 GoF《设计模式》一书。其核心思想是:定义一系列算法,把它们各自封装起来,并使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。
用一句更工程化的话概括:
策略模式把"做什么(what)"和"怎么做(how)“分离——上层代码只知道"我需要一个能解码的东西并调用它”,至于这个东西内部具体是贪心还是束搜索,是运行时可插拔的细节。
GoF 对策略模式的经典定义有三个关键词,建议逐字理解:
- 定义一系列算法(a family of algorithms):这些算法要解决同一类问题,拥有统一的输入输出接口;
- 封装起来(encapsulate each one):每种算法各自独立成一个类/对象,内部实现细节对外不可见;
- 可以相互替换(interchangeable):客户端代码持有的是抽象接口引用,具体指向哪个算法对象,可以在运行时决定和更换。
策略模式的结构
Context(上下文,持有一个策略引用) │ has-a ▼ Strategy(抽象策略接口) ├── ConcreteStrategyA (贪心解码) ├── ConcreteStrategyB (束搜索解码) └── ConcreteStrategyC (WFST解码)三个角色:
- Strategy(策略接口):定义所有具体策略必须实现的统一方法签名;
- ConcreteStrategy(具体策略):实现某一种具体算法;
- Context(上下文):持有一个指向
Strategy的引用/指针,不关心具体是哪种策略,只负责在合适的时机调用它;通常还负责提供"设置/切换策略"的接口。
这个结构和工厂模式长得不一样,但常常配合使用:工厂模式负责"根据条件创建出正确的策略对象",策略模式负责"上下文如何使用这个策略对象"——后文会专门讲两者的关系。
从零实现:一步步重构
我们把开头那个if-else版本的解码器,逐步重构为策略模式,让你直观感受"底层发生了什么变化"。
第一步:抽取统一接口
所有解码算法的共性是:输入 logits(模型输出的概率分布),输出解码文本。把这个共性抽成接口:
#include<vector>#include<string>classDecodeStrategy{public:virtual~DecodeStrategy()=default;virtualstd::stringDecode(conststd::vector<float>&logits)const=0;};第二步:每种算法独立成类
#include<iostream>classGreedyDecodeStrategy:publicDecodeStrategy{public:std::stringDecode(conststd::vector<float>&logits)