【设计模式精讲】26.策略模式(Strategy)

【设计模式精讲】26.策略模式(Strategy)

【摘要】:第一篇埋的伏笔到此兑现------反面教材一号 calcPrice 的 switch 又长回来了,二十七个分支,每加一种定价全函数回归。本文从「算法平行可换」与「状态依次迁移」的分野讲起,给出策略模式的 GoF 意图:定义算法族、各自封装、彼此可互换,让算法独立于使用它的客户变化;C++98 版把定价规则做成可注入的策略对象(自带费率与门槛数据),现代版给出 std::function 策略、模板策略(STL 比较器的形态)、以及把两个策略组合成链的高阶函数。C++ 的特殊地位值得一提:迭代器之后,策略是 STL 的第二根支柱。文末对照标准库的策略参数、absl::Hash 与 folly::Poly。读完你能把「满屏 if-else 的算法选择」翻译成「一族可插拔的算法」。

【关键词】:策略模式、算法族、可互换、依赖注入、std::function、模板策略

【代码基准】:C++17

1. 反面教材一号,二十七个分支

第 1 篇开篇立过一块反面教材:订单按类型计价的函数。当时只写了三个分支,现在看看它长成了什么样:

cpp 复制代码
// 说明性片段
// ❌ 第 1 篇的反面教材 1 号,两年后
double calcPrice(OrderType t, double raw) {
  switch (t) {
    case Normal:    return raw;
    case Promo:     return raw * 0.9;
    case Vip:       return raw * 0.85;
    case Wholesale: return raw * 0.75;
    case Employee:  return raw * 0.6;
    // ......此处省略二十二个分支
    default:        return raw;
  }
}

第 1 篇诊断过的病如今全部应验:「新增一种定价」这个变化落在了「已有代码的修改」上------每加一个分支,全部既有分支进回归;二十七种定价挤在一个函数里,谁也不敢动;测试只能整函数跑,单独验证「员工价」的规则要从入口喂枚举。更糟的新需求还在路上:营销要「VIP 叠加满 300 减 50」------分支之间的关系从平行变成了组合,switch 彻底表达不了;定价规则要按运营配置在运行时切换------重编译发版。

注意它与第 25 篇的 switch 不同色:订单状态 是依次迁移的生涯(Created→Paid→Shipped),定价类型是平行的选项------VIP 并不「迁移成」员工价,它们只是同一问题的不同解法。「平行的解法族、要能增、要能换、最好还能组合」------这就是策略模式的地盘,也是对第 1 篇那句「第 26 篇会回来重写它」的兑现现场。

2. 模式意图与定义

  • 一句话定义 :定义一系列算法,把每一个算法封装起来,并且使它们可以互换。策略模式让算法独立于使用它的客户而变化。
    解决的问题:同一问题的多种解法需要增删、替换、组合与配置,而调用方只想面对「一个算法」。
  • GoF 原文意图Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. (定义一系列算法,一个个封装起来。)关键词 family(族)interchangeable(可互换)------解法必须成族且平行,才配叫策略。
  • Refactoring Guru 的表述:策略模式能让你定义一系列算法,并将每种算法分别放入独立的类中,使算法的对象能够相互替换;RG 用「去机场有多种交通方式」作比------开车、公交、打车是平行选项,选哪个不影响「到达机场」这件事本身。

三条定性:

  1. 算法自带配置、自足完事 。VIP 策略自带费率、满减策略自带门槛------与第 25 篇「数据全在上下文、状态零数据」正好相反,策略类携带自己的参数,因为每个策略的参数表本来就不一样;
  2. 调用方只认抽象 。结账流程对 PricePolicy* 编程,今天注入谁、明天换成谁,流程一行不改;
  3. C++ 的特殊地位 :继第 21 篇迭代器之后,策略是 STL 的第二根支柱 ------sort 的比较器、unordered_map 的哈希与相等、容器的分配器,全是「模板参数形状的策略」。可以说 C++ 程序员每天都在用策略模式,只是它藏在尖括号里。

3. UML 图 + 结构说明

#mermaid-svg-nwDswehCzizSyygg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nwDswehCzizSyygg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nwDswehCzizSyygg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nwDswehCzizSyygg .error-icon{fill:#552222;}#mermaid-svg-nwDswehCzizSyygg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nwDswehCzizSyygg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nwDswehCzizSyygg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nwDswehCzizSyygg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nwDswehCzizSyygg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nwDswehCzizSyygg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nwDswehCzizSyygg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nwDswehCzizSyygg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nwDswehCzizSyygg .marker.cross{stroke:#333333;}#mermaid-svg-nwDswehCzizSyygg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nwDswehCzizSyygg p{margin:0;}#mermaid-svg-nwDswehCzizSyygg g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-nwDswehCzizSyygg g.classGroup text .title{font-weight:bolder;}#mermaid-svg-nwDswehCzizSyygg .cluster-label text{fill:#333;}#mermaid-svg-nwDswehCzizSyygg .cluster-label span{color:#333;}#mermaid-svg-nwDswehCzizSyygg .cluster-label span p{background-color:transparent;}#mermaid-svg-nwDswehCzizSyygg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nwDswehCzizSyygg .cluster text{fill:#333;}#mermaid-svg-nwDswehCzizSyygg .cluster span{color:#333;}#mermaid-svg-nwDswehCzizSyygg .nodeLabel,#mermaid-svg-nwDswehCzizSyygg .edgeLabel{color:#131300;}#mermaid-svg-nwDswehCzizSyygg .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-nwDswehCzizSyygg .label text{fill:#131300;}#mermaid-svg-nwDswehCzizSyygg .labelBkg{background:#ECECFF;}#mermaid-svg-nwDswehCzizSyygg .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-nwDswehCzizSyygg .classTitle{font-weight:bolder;}#mermaid-svg-nwDswehCzizSyygg .node rect,#mermaid-svg-nwDswehCzizSyygg .node circle,#mermaid-svg-nwDswehCzizSyygg .node ellipse,#mermaid-svg-nwDswehCzizSyygg .node polygon,#mermaid-svg-nwDswehCzizSyygg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nwDswehCzizSyygg .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg g.clickable{cursor:pointer;}#mermaid-svg-nwDswehCzizSyygg g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-nwDswehCzizSyygg g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-nwDswehCzizSyygg .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-nwDswehCzizSyygg .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-nwDswehCzizSyygg .dashed-line{stroke-dasharray:3;}#mermaid-svg-nwDswehCzizSyygg .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-nwDswehCzizSyygg #compositionStart,#mermaid-svg-nwDswehCzizSyygg .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg #compositionEnd,#mermaid-svg-nwDswehCzizSyygg .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg #dependencyStart,#mermaid-svg-nwDswehCzizSyygg .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg #dependencyStart,#mermaid-svg-nwDswehCzizSyygg .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg #extensionStart,#mermaid-svg-nwDswehCzizSyygg .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg #extensionEnd,#mermaid-svg-nwDswehCzizSyygg .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg #aggregationStart,#mermaid-svg-nwDswehCzizSyygg .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg #aggregationEnd,#mermaid-svg-nwDswehCzizSyygg .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg #lollipopStart,#mermaid-svg-nwDswehCzizSyygg .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg #lollipopEnd,#mermaid-svg-nwDswehCzizSyygg .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-nwDswehCzizSyygg .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-nwDswehCzizSyygg .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nwDswehCzizSyygg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nwDswehCzizSyygg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nwDswehCzizSyygg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 注入(借用或持有)
<<interface>>
PricePolicy
+apply(raw) : double
NormalPrice
+apply(raw) : double
VipPrice
-rate_ double
+apply(raw) : double
ThresholdPrice
-threshold_ double
-cut_ double
+apply(raw) : double
Checkout
-policy_ PricePolicy
+total(items) : double

三个参与者:

  • 策略(Strategy)接口PricePolicy,声明算法的公共入口------签名统一,是「可互换」的全部前提;
  • 具体策略(Concrete Strategy)NormalPrice/VipPrice/ThresholdPrice------各自携带算法与算法的配置(费率、门槛);
  • 上下文(Context)Checkout------持一个策略引用,把「怎么算」整体委托出去,自己只保留「算什么、何时算」。

把这张图与第 25 篇状态模式并排:拓扑完全相同(上下文组合接口、逐调用委托),差别全在语义注脚------策略之间没有箭头(平行解法、互不相识),状态之间有迁移箭头(互为目标);策略由外部注入、一次性定,状态由对象自己迁移、随时在变。「结构相同、意图相反」的第三对(前两对:装饰器/代理、外观/中介者)就此集齐。

4. 传统 C++ 写法(C++11 之前)

C++98 完整形态:策略抽象基类、具体策略自带配置、上下文构造注入:

cpp 复制代码
// C++98/03 写法
#include <cstdio>
#include <vector>

// ---- 策略接口 ----
class PricePolicy {
public:
  virtual ~PricePolicy() {}

  virtual double apply(
      double raw) const = 0;

protected:
  PricePolicy() {}

private:
  PricePolicy(const PricePolicy&);
  PricePolicy& operator=(
      const PricePolicy&);
};

// ---- 具体策略:自带算法与配置 ----
class NormalPrice : public PricePolicy {
public:
  double apply(double raw) const {
    return raw;
  }
};

class VipPrice : public PricePolicy {
public:
  explicit VipPrice(double rate)
      : rate_(rate) {}

  double apply(double raw) const {
    return raw * rate_;
  }

private:
  double rate_;        // 策略自足的配置
};

class ThresholdPrice
    : public PricePolicy {
public:
  ThresholdPrice(double threshold,
                 double cut)
      : threshold_(threshold),
        cut_(cut) {}

  double apply(double raw) const {
    return raw > threshold_
        ? raw - cut_
        : raw;
  }

private:
  double threshold_;
  double cut_;
};

// ---- 上下文:只管流程,算法注入 ----
class Checkout {
public:
  explicit Checkout(
      const PricePolicy* policy)
      : policy_(policy) {}

  double total(
      const std::vector<double>&
          items) const {
    double sum = 0;
    for (size_t i = 0;
         i < items.size(); ++i) {
      sum += policy_->apply(
          items[i]);
    }
    return sum;
  }

private:
  const PricePolicy* policy_;
  // 借用:策略归装配方所有,
  // 生命周期长于本次结账
};

int main() {
  VipPrice vip(0.85);
  ThresholdPrice fullCut(300, 50);

  std::vector<double> cart;
  cart.push_back(199.0);
  cart.push_back(149.0);

  Checkout c1(&vip);
  Checkout c2(&fullCut);

  printf("VIP 价:%.2f\n",
         c1.total(cart));   // 296.15
  printf("满减价:%.2f\n",
         c2.total(cart));   // 298.00
  return 0;
}

对照第 1 节的 switch:加「学生价」= 新写一个策略类,calcPrice 与全部旧策略零改动;「VIP 叠加满减」的答案在第 5 节(策略可以组合);每个策略单独实例化、单独测试------回归范围从「全函数」缩到「一个类」。

三条传统写法的铁律:策略自足 ------费率、门槛随身携带,绝不回头读全局配置;注入发生在装配期 ------上下文对策略「只借不拥」,生命周期约定写进注释(现代版可用智能指针收紧);签名一旦定族,慎改 ------策略接口是全族的公共分母,改它等于全族返工,宁可窄接口(double(double))起步。

5. 现代 C++ 进阶写法

升级零:std::function 策略------族的最小形态。算法能被闭包完整表达时,类层次整个让位给可调用对象:

cpp 复制代码
// 节选:lambda 策略 + 装配
#include <functional>

using PriceFn =
    std::function<double(double)>;

double vipRate = 0.85;
PriceFn vip = [vipRate](double raw) {
  return raw * vipRate;
};

PriceFn normal = [](double raw) {
  return raw;
};

// Checkout 改持 PriceFn,
// 结账流程一行不变

第 1 节那个二十七分支的 switch,此刻变成一张 map<string, PriceFn> 的配置表------「加一种定价」从改代码降级为加一行数据。

改进一:模板策略------STL 比较器的形态 。策略作为类型参数在编译期织入:零虚函数、可内联、无堆分配:

cpp 复制代码
// 节选:编译期策略
#include <cstdio>
#include <vector>

template <typename Policy>
class FastCheckout {
public:
  explicit FastCheckout(Policy p)
      : policy_(p) {}

  double total(
      const std::vector<double>&
          items) const {
    double sum = 0;
    for (double x : items)
      sum += policy_.apply(x);
    return sum;
  }

private:
  Policy policy_;
};

// FastCheckout<VipPrice> 与
// FastCheckout<ThresholdPrice>
// 是两个静态类型------运行时不可换,
// 换来内联与零间接

这正是 std::sort 的比较器、std::set 的排序准则的形态。取舍与前几篇的「模板换运行期多态」完全一致:策略在编译期可定、调用在热路径,用模板;策略要运行时按配置选,用 std::function 或虚接口

改进二:策略组合器------高阶函数叠出「VIP 再满减」。第 1 节末尾那个 switch 表达不了的需求,用组合器一行解决:

cpp 复制代码
#include <functional>

// 两个策略串成一条:先 a 后 b
PriceFn both(PriceFn a, PriceFn b) {
  return [a, b](double x) {
    return b(a(x));
  };
}

// 装配:VIP 打完折后再看满减
PriceFn vipThenCut = both(
    [](double x) { return x * 0.85; },
    [](double x) {
      return x > 300 ? x - 50 : x;
    });
// 480 的原价 → 408 → 358

这是第 13 篇装饰器洋葱的函数化(那篇 §5 改进一同型),也是第 20 篇规则树的数据化前奏------模式在函数一级相遇时,边界本来就很模糊

展望 :C++20 concepts 给模板策略写显式契约(「可调用 double(double)」),约束失败从天书变成人话;策略清单配置化后,下一步自然是「让运营自己写公式」------第 20 篇解释器在那里候场。

6. 优缺点与适用场景

  • ✅ 优点(GoF 后果清单):运行时换算法 (配置驱动、A/B 实验、按用户分层);消除条件语句 ------平行解法各自成类,switch 蔓延终结;算法族可独立演化与复用------每个策略单独测试、单独发布,还能在别处整族复用(比较器、哈希、序列化器)。
  • ❌ 缺点:类与对象数量上升 (lambda 化能缓解);选择负担仍在 ------客户端(或装配层)必须知道有哪些策略、怎么选,常需工厂(第 6 篇)或配置表兜底;std::function 与虚调用各有一层间接与可能的堆分配,纳秒级热路径慎用;策略接口的公共分母设计过肥,会让每个实现背一堆空方法。
  • 🎯 适用场景:同一问题的平行解法会持续增加或运行时切换------定价/路由/压缩级别/序列化格式/重试策略;作为库的定制点(比较器、哈希、分配器------STL 的传统艺能);消除「按枚举选算法」的大 switch。

〔辨析〕策略 vs 状态(第 25 篇,正面回收) :结构全同、三处分野------驱动方(外部注入 vs 自己迁移)、相识性(平行陌生 vs 互为迁移目标)、建模对象(算法插头 vs 对象生涯)。策略 vs 命令(第 19 篇) :长命的算法(被反复调用、随时替换)vs 一次请求的物化(入栈、撤销、回放);把命令误当策略,撤销栈里会堆满「算法」。策略 vs 模板方法(第 27 篇预告) :组合换整个 算法 vs 继承填部分 步骤------「委托 vs 继承」这对老冤家在行为型的正面对决,下一篇主场清算。策略 vs 解释器(第 20 篇):替换单个算法 vs 执行任意句子;当「策略」多到要用一门小语言描述时,就跨过去了。

7. 开源项目中的身影

标准库:策略是 STL 的第二根支柱。继迭代器(第 21 篇)之后,标准库把策略做成了「类型参数」的传统:

cpp 复制代码
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>

std::vector<std::string> names{
    "chen", "an", "bo"};

// 策略一:比较器------按长度排序
std::sort(names.begin(), names.end(),
          [](const std::string& a,
             const std::string& b) {
            return a.size() < b.size();
          });

// 策略二:自定义哈希------
// 不区分大小写的键
struct CiHash {
  size_t operator()(
      std::string_view s) const {
    size_t h = 0;
    for (char c : s)
      h = h * 31 + tolower(c);
    return h;
  }
};
std::unordered_map<
    std::string, int, CiHash>
    ciIndex;

点评:sort 的第三参、unordered_map 的第三四参、容器的分配器------策略参数在标准库里有一整套「尖括号语法」。自定义比较器或哈希时,你写的就是本篇的具体策略。

Abseil:absl::Hash,可扩展的哈希策略框架 。Abseil 把「怎么哈希」做成正式的扩展点:用户为自己的类型实现 AbslHashValue,即可直接用作 absl::flat_hash_map 的键------无需特化 std::hash、不受标准库哈希演进的拖累:

cpp 复制代码
// 说明性片段(需链接 absl)
struct Point {
  int x, y;
  template <typename H>
  friend H AbslHashValue(H h,
                         const Point& p) {
    return H::combine(std::move(h),
                      p.x, p.y);
  }
};
// absl::flat_hash_map<Point, int>
// 的键槽策略就地打通

点评:框架把「策略族」的扩展权下放给用户、又保证全族语义一致(combine 的次序敏感性有文档约定)------大型库里「策略即扩展点」的范式样本。

Folly:folly::Poly,无继承的运行时多态底座std::function 只能包装「一个签名」,folly::Poly 把「一组签名」做成接口级别的类型擦除------不继承、无虚表,运行期可替换:

cpp 复制代码
// 说明性片段(需包含 folly/Poly.h,
// 节选)
struct Drawable : folly::PolyVal {}...
// folly::Poly<Drawable> 持有任何
// "实现了 draw() 的类型",
// 像接口,但没有继承

点评:它瞄准的正是虚函数策略的两个痛点------跨 ABI 边界与非侵入式适配。策略模式走到工业深处,「怎么实现一个可替换接口」本身成了可替换的策略。

三份代码合看:标准库把策略做进类型参数、Abseil 把策略做成扩展协议、Folly 把策略的底座也策略化------「算法可换」这四个字,是 C++ 生态最重度复用的模式红利。

本篇小结

同一问题的平行解法要增、要换、要组合时,别让它们挤在一个 switch 里互咬:定义一族策略、各自封装、自带配置,上下文注入使用------第一篇的 calcPrice 至此兑现承诺,「加一种定价」降级为「加一个类或一行配置」。三档实现按需取:虚接口是最通用的族,std::function 是最轻的族,模板参数是最快的族;组合器让族与族之间还能串联。与状态的三问(谁驱动、认不认识彼此、建模什么)值得背下来------这是结构最易混淆的一对模式。下一篇模板方法模式,把「换整个算法」缩小成「固定骨架、只填几个空」:委托与继承的正面对决。

本文模式定义与角色划分参考了 Refactoring Guru《设计模式》中文版「策略」一章,意图译文与后果清单参考了 GoF《Design Patterns》第 5 章 Strategy 一节。

相关推荐
果果燕2 小时前
实习笔记(六)主机按钮状态上报完整流程
开发语言·c++·php
是个西兰花3 小时前
网络基础1
linux·网络·c++·智能路由器
Ysx3 小时前
AI 问答不再"一本正经地胡说":30 轮提示词迭代,沉淀 4 条防幻觉铁律
设计模式
老赵的博客3 小时前
c++QT之动态库加载常见报错
c++·qt
wabs6663 小时前
关于二叉树【429.N叉树的层序遍历的思考】
数据结构·c++·算法·leetcode·二叉树
hetao17338373 小时前
2026-09-08 hetao1733837 的刷题记录
c++·算法
码匠许师傅4 小时前
【设计模式精讲】25.状态模式(State)
c++·ui·设计模式·状态模式·uml
C++ 老炮儿的技术栈4 小时前
MFC CPtrArray的用法
开发语言·数据结构·c++·算法·mfc·c
佳児素花痴╮4 小时前
C++基础速通
开发语言·c++