Day 20:策略模式(Strategy Pattern)

行为型模式 GoF 23 · 第 20 天 / 共 23 天 C++17 / Qt 6 2026-09-03

①今日主题

策略模式(Strategy Pattern):定义一族算法(或业务规则),将每一个算法分别封装成独立的对象并使它们实现同一个抽象接口,从而让它们可以在运行时互相替换——算法的变化独立于使用算法的客户端。它属于 GoF 23 种设计模式中的行为型模式,别名 Policy(政策/策略)。

一句话记忆:「把会变的算法装进可替换的盒子里,上下文只认接口、不认实现。」

今天是我们行为型模式之旅的第二站。上一站(Day 19)的状态模式关注「对象内部状态变化时行为如何自动切换」,今天的策略模式则关注「同一件事有多种做法时,如何让做法本身可以被自由插拔」。两者代码结构极其相似,但意图截然不同,第 ⑪ 节会专门辨析。

②为什么需要这个模式

先看一段没有策略模式、但几乎所有真实项目里都出现过的「坏代码」。假设电商系统要按用户类型计算订单折后价:

// 坏味道示例:类型枚举 + 集中式 switch 分支
enum class UserType { Normal, Student, Vip, Enterprise };

// 价格计算:随着用户类型和促销活动增多,这个函数会越来越臃肿
double calcFinalPrice(double price, UserType type, bool withCoupon) {
    double p = price;
    switch (type) {
    case UserType::Normal:              break;               // 无折扣
    case UserType::Student:   p *= 0.85; break;              // 学生 85 折
    case UserType::Vip:       p *= 0.70; break;              // VIP 7 折
    case UserType::Enterprise:p *= 0.90; break;              // 企业 9 折
    }
    if (withCoupon && p >= 200.0) p -= 50.0;   // 满 200 减 50 的优惠券
    return p;
}

这段代码在功能上是正确的,但随着业务演进,问题会集中爆发:

从 SOLID 原则角度审视这段坏代码:

核心矛盾:计价算法是易变的、可独立演进的,结算流程是稳定的。把两者硬揉进同一个函数,等于让稳定的部分跟着易变的部分一起频繁返工。策略模式的做法是:用「组合 + 多态」把易变的算法从稳定的流程中剥离出来,各自封装、各自演化、运行时可替换。

③核心思想

策略模式的本质只有三句话:

  1. 为一系列算法定义一个统一的抽象接口(Strategy);
  2. 每个算法写成一个独立的类(ConcreteStrategy),各自实现该接口;
  3. 使用算法的上下文(Context)不再自己写算法,而是持有一个策略接口的引用,把计算请求委托给当前持有的策略对象——想换算法?换掉这个引用即可。

通俗地讲:Context 是「老板」,策略是「员工」。老板不亲自干活,他只认「能干这活的员工」这个身份(接口),具体派哪个员工上,随时可以换。老板的日常工作流程(接单、验收、交付)完全不受换人影响。

生活类比 1 —— 手机地图导航:你的目的地(Context 的业务目标)是固定的,但「驾车」「公交」「骑行」「步行」是四种不同的路线算法(策略)。地图 App 的框架不会因为你切换出行方式而重写,它只是把你选择的交通方式对应的算法插进去,然后调用「计算路线」这个统一入口。App 与具体算法互不干扰,算法之间也互不知晓。

生活类比 2 —— 商场收银台:收银流程(扫商品 → 报价 → 收款 → 出小票)是固定的;但「现金」「银行卡」「扫码支付」「数字人民币」是四种支付算法。收银员不需要知道每种支付的内部规则,只要顾客说「扫码」,就调用扫码策略。未来新增一种支付方式,收银台和收银机程序一行都不用改,只需要「注册」一个新的支付策略。

这两个类比揭示了策略模式的灵魂:把「做什么」(稳定的业务骨架)与「怎么做」(易变的算法细节)分离,变化的部分通过接口插拔,稳定部分永不触碰变化部分。

④UML 与角色关系

策略模式的结构图非常简洁,只有三个角色 + 一个客户端:

                    ┌──────────────────────┐
                    │      Context         │  (上下文:如 Checkout 结算器)
                    │ ┌──────────────────┐ │
             委托 ──▶│ │  m_strategy: 接口*│ │  持有策略的引用/指针
                    │ └──────────────────┘ │
                    └──────────┬───────────┘
                               │ 调用统一接口
                    ┌──────────▼───────────┐
                    │   «interface»        │
                    │   Strategy           │  (抽象策略)
                    │   +algorithm()       │
                    └──────────┬───────────┘
             ┌─────────────────┼─────────────────┐
     ┌───────▼───────┐ ┌───────▼───────┐ ┌───────▼───────┐
     │ ConcreteA     │ │ ConcreteB     │ │ ConcreteC     │  (具体策略)
     │ +algorithm()  │ │ +algorithm()  │ │ +algorithm()  │
     └───────────────┘ └───────────────┘ └───────────────┘
                ▲
                │ 实例化(由客户端/工厂创建后注入 Context)
角色名字职责变化维度
抽象策略Strategy定义算法族统一的公共接口(通常只有一个核心方法,如 calc/render/sort)稳定,几乎不变
具体策略ConcreteStrategyA/B/C…各自实现一种算法/规则,互不感知、互不依赖易变:新增算法 = 新增一个类
上下文Context持有 Strategy 引用;把业务请求转发(委托)给策略;不关心具体是哪个策略稳定:算法更换不影响它
客户端Client创建具体策略对象并注入 Context(构造注入或 set 注入),决定「当前用哪个策略」只与抽象策略打交道

协作时序:① 客户端实例化一个具体策略;② 把它注入 Context(构造函数或 setStrategy);③ 客户端调用 Context 的业务方法;④ Context 把请求转发给持有的策略对象;⑤ 策略对象执行自己的算法并返回结果;⑥ 需要换算法时,客户端再注入另一个策略对象,Context 内部代码零改动。

注意两个实现细节:其一,Context 与 Strategy 是组合(聚合)关系而非继承关系——这是「组合优于继承」的典型体现;其二,策略对象可以是无状态的(只有算法没有数据成员),此时可以做成共享单例供多个 Context 复用,进一步节省内存。

⑤最小 C++17 示例

以第 ② 节的电商折扣为例,用策略模式重构。核心变化:UserType 枚举 + switch 消失,代之以一个抽象接口和四个策略类;结算器 Checkout(Context)持有一个 std::unique_ptr 策略指针,运行时可更换。完整可编译,无需任何第三方库:

#include <iostream>
#include <memory>
#include <string>

// ========== 抽象策略:所有「优惠算法」的统一接口 ==========
class IDiscountStrategy {
public:
    virtual ~IDiscountStrategy() = default;   // 基类必须虚析构
    virtual double calc(double price) const = 0;  // 输入原价,返回应付价
    virtual std::string name() const = 0;         // 策略名(演示/日志用)
};

// ========== 具体策略 A:普通用户,无折扣 ==========
class NormalDiscount final : public IDiscountStrategy {
public:
    double calc(double price) const override { return price; }
    std::string name() const override { return "普通用户(无折扣)"; }
};

// ========== 具体策略 B:学生 85 折 ==========
class StudentDiscount final : public IDiscountStrategy {
public:
    double calc(double price) const override { return price * 0.85; }
    std::string name() const override { return "学生(85折)"; }
};

// ========== 具体策略 C:VIP 7 折 ==========
class VipDiscount final : public IDiscountStrategy {
public:
    double calc(double price) const override { return price * 0.70; }
    std::string name() const override { return "VIP(7折)"; }
};

// ========== 具体策略 D:满 200 减 50(活动规则也是策略) ==========
class FullReductionDiscount final : public IDiscountStrategy {
public:
    double calc(double price) const override {
        return price >= 200.0 ? price - 50.0 : price;
    }
    std::string name() const override { return "满200减50(活动)"; }
};

// ========== 上下文 Context:持有当前策略,转发计价请求 ==========
class Checkout {
public:
    // 构造注入:默认策略
    explicit Checkout(std::unique_ptr<IDiscountStrategy> s)
        : m_strategy(std::move(s)) {}

    // set 注入:运行时动态更换算法(旧策略随 unique_ptr 自动析构,无泄漏)
    void setStrategy(std::unique_ptr<IDiscountStrategy> s) {
        m_strategy = std::move(s);
    }

    double settle(double price) const {
        return m_strategy->calc(price);   // 委托给当前策略
    }
    std::string strategyName() const { return m_strategy->name(); }

private:
    std::unique_ptr<IDiscountStrategy> m_strategy;  // 组合优于继承
};

int main() {
    const double price = 240.0;   // 购物车原价
    std::cout << "商品原价: " << price << " 元\n\n";

    // 默认策略:普通用户
    Checkout checkout(std::make_unique<NormalDiscount>());
    std::cout << "[" << checkout.strategyName() << "] 应付: "
              << checkout.settle(price) << " 元\n";

    // 运行中换策略:学生结账
    checkout.setStrategy(std::make_unique<StudentDiscount>());
    std::cout << "[" << checkout.strategyName() << "] 应付: "
              << checkout.settle(price) << " 元\n";

    // 运行中换策略:VIP 结账
    checkout.setStrategy(std::make_unique<VipDiscount>());
    std::cout << "[" << checkout.strategyName() << "] 应付: "
              << checkout.settle(price) << " 元\n";

    // 运行中换策略:活动满减(业务方无需改动 Checkout 一行代码)
    checkout.setStrategy(std::make_unique<FullReductionDiscount>());
    std::cout << "[" << checkout.strategyName() << "] 应付: "
              << checkout.settle(price) << " 元\n";

    return 0;   // 所有对象在作用域结束时由 RAII 自动释放
}

程序输出:

商品原价: 240 元

[普通用户(无折扣)] 应付: 240 元
[学生(85折)] 应付: 204 元
[VIP(7折)] 应付: 168 元
[满200减50(活动)] 应付: 190 元

对比第 ② 节的坏代码:现在「加一种折扣」=「写一个新类 + 在 main 里注入一次」,Checkout、其他策略类、调用点统统不用改。代码量虽然略增,但每个类的职责单一、可单独测试、可独立复用——这正是用少量结构换长期可维护性。

⑥Qt 实战示例

在写自己的策略代码之前,先看一个惊人的事实:你每天用的 Qt 本身就是一个策略模式的巨型教科书案例——QStyle。Qt 控件不自己画自己:QPushButton 要绘制时,会调用 style()->drawControl(CE_PushButtonLabel, ...),把绘制请求委托给当前的外观策略对象。Windows 风格、Fusion 风格、macOS 风格……就是一组可互相替换的「绘制算法策略」;QStyleFactory::create("Fusion") + QApplication::setStyle(...) 一行代码即可在运行时切换整套 UI 外观,而所有控件代码零改动。这就是为什么同一套 Qt 程序在 Windows、Linux、macOS 上长着不同的脸——不是控件写了三份,而是插了三种不同的 QStyle 策略。

下面我们自己实现一个完整的策略模式 Qt 6 程序:文档导出器——同一篇文档,可以导出为纯文本 / HTML / Markdown 三种格式,下拉框一换,导出算法即换。工程共四个文件,均为 Qt6 + C++17 标准写法。

exporter.h(抽象策略 + 三个具体策略 + 上下文):

#ifndef EXPORTER_H
#define EXPORTER_H

#include <QString>

// ========== 抽象策略:所有「导出算法」的统一接口 ==========
class IExportStrategy {
public:
    virtual ~IExportStrategy() = default;
    virtual QString name() const = 0;                    // 策略名(下拉框显示)
    virtual QString render(const QString &title,
                           const QString &body) const = 0; // 渲染为目标格式
};

// ========== 具体策略 1:纯文本 ==========
class PlainTextExporter final : public IExportStrategy {
public:
    QString name() const override { return QStringLiteral("纯文本 (.txt)"); }
    QString render(const QString &title,
                   const QString &body) const override;
};

// ========== 具体策略 2:HTML 网页 ==========
class HtmlExporter final : public IExportStrategy {
public:
    QString name() const override { return QStringLiteral("HTML 网页 (.html)"); }
    QString render(const QString &title,
                   const QString &body) const override;
};

// ========== 具体策略 3:Markdown ==========
class MarkdownExporter final : public IExportStrategy {
public:
    QString name() const override { return QStringLiteral("Markdown (.md)"); }
    QString render(const QString &title,
                   const QString &body) const override;
};

// ========== 上下文 Context:持有当前导出策略并转发请求 ==========
class DocumentExporter {
public:
    explicit DocumentExporter(IExportStrategy *strategy = nullptr);

    void setStrategy(IExportStrategy *strategy);  // 运行时更换导出算法
    IExportStrategy *strategy() const;

    // 把导出请求委托给当前策略;未设置策略时返回空串
    QString exportDocument(const QString &title,
                           const QString &body) const;

private:
    IExportStrategy *m_strategy = nullptr;  // 非拥有指针:策略由外部管理生命周期
};

#endif // EXPORTER_H

exporter.cpp(具体策略实现):

#include "exporter.h"

QString PlainTextExporter::render(const QString &title,
                                  const QString &body) const {
    return title + QStringLiteral("\n")
           + QStringLiteral("--------------------")
           + QStringLiteral("\n") + body;
}

QString HtmlExporter::render(const QString &title,
                             const QString &body) const {
    // toHtmlEscaped():把正文中的 <>& 转义,避免破坏 HTML 结构
    return QStringLiteral("<!DOCTYPE html>\n<html>\n<head>\n")
           + QStringLiteral("<meta charset=\"utf-8\">\n<title>")
           + title.toHtmlEscaped() + QStringLiteral("</title>\n</head>\n")
           + QStringLiteral("<body>\n<h1>") + title.toHtmlEscaped()
           + QStringLiteral("</h1>\n<p>") + body.toHtmlEscaped()
           + QStringLiteral("</p>\n</body>\n</html>\n");
}

QString MarkdownExporter::render(const QString &title,
                                 const QString &body) const {
    return QStringLiteral("# ") + title + QStringLiteral("\n\n") + body;
}

DocumentExporter::DocumentExporter(IExportStrategy *strategy)
    : m_strategy(strategy) {}

void DocumentExporter::setStrategy(IExportStrategy *strategy) {
    m_strategy = strategy;   // 运行时替换算法对象
}

IExportStrategy *DocumentExporter::strategy() const { return m_strategy; }

QString DocumentExporter::exportDocument(const QString &title,
                                         const QString &body) const {
    if (!m_strategy)
        return {};
    return m_strategy->render(title, body);   // 委托:Context 不关心具体算法
}

main.cpp(客户端:组装界面,用下拉框切换策略):

#include <QApplication>
#include <QComboBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QWidget>

#include <vector>
#include "exporter.h"

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    // 策略对象池:栈上创建,生命周期覆盖整个窗口,故 Context 可用非拥有指针
    PlainTextExporter plain;
    HtmlExporter      html;
    MarkdownExporter  markdown;
    std::vector<IExportStrategy *> strategies{&plain, &html, &markdown};

    // 上下文:先注入默认策略(纯文本)
    DocumentExporter exporter(&plain);

    // ---------- 组装界面 ----------
    QWidget window;
    window.setWindowTitle(QStringLiteral("策略模式演示:文档导出器"));

    auto *titleEdit = new QLineEdit(QStringLiteral("C++/Qt 设计模式学习笔记"));
    auto *bodyEdit  = new QTextEdit(QStringLiteral(
        "今天是第 20 天,学习策略模式。\n算法与上下文解耦,运行时自由替换。"));
    auto *combo     = new QComboBox;
    combo->addItem(plain.name());
    combo->addItem(html.name());
    combo->addItem(markdown.name());

    auto *exportBtn = new QPushButton(QStringLiteral("导出预览"));
    auto *result    = new QLabel(QStringLiteral("(预览结果将显示在这里)"));
    result->setWordWrap(true);
    result->setTextInteractionFlags(Qt::TextSelectableByMouse);
    result->setAlignment(Qt::AlignTop | Qt::AlignLeft);

    // ---------- 信号槽:切换下拉框 = 运行时更换策略 ----------
    QObject::connect(combo, &QComboBox::currentIndexChanged,
                     [&](int index) {
        if (index >= 0 && index < static_cast<int>(strategies.size()))
            exporter.setStrategy(strategies[static_cast<std::size_t>(index)]);
    });

    // 点击按钮 = 把渲染请求委托给当前策略
    QObject::connect(exportBtn, &QPushButton::clicked, [&]() {
        result->setText(exporter.exportDocument(titleEdit->text(),
                                                bodyEdit->toPlainText()));
    });

    // ---------- 布局 ----------
    auto *root = new QVBoxLayout(&window);
    root->addWidget(new QLabel(QStringLiteral("标题:")));
    root->addWidget(titleEdit);
    root->addWidget(new QLabel(QStringLiteral("正文:")));
    root->addWidget(bodyEdit);
    auto *row = new QHBoxLayout;
    row->addWidget(new QLabel(QStringLiteral("导出格式:")));
    row->addWidget(combo, 1);
    row->addWidget(exportBtn);
    root->addLayout(row);
    root->addWidget(result, 1);

    window.resize(560, 480);
    window.show();
    return app.exec();   // 窗口为栈对象,退出时自动销毁全部子控件
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(strategy_demo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)

find_package(Qt6 REQUIRED COMPONENTS Widgets)

qt_add_executable(strategy_demo
    main.cpp
    exporter.cpp
    exporter.h
)
target_link_libraries(strategy_demo PRIVATE Qt6::Widgets)

编译运行:cmake -B build && cmake --build build && ./build/strategy_demo。在正文里输入文字、选择「HTML 网页」再点「导出预览」,你会看到 DocumentExporter 的代码一行没变,输出的却是完全不同的格式——这就是策略「可替换」的直观体验。若想真正落盘,只需在槽函数里加一个 QFile 写入,属于练习范畴(见第 ⑭ 节)。

Qt 用法小结:本例中策略对象在栈上创建、用非拥有指针注入 Context,符合 Qt「对象树管理大对象、策略类小而轻」的惯例;如果策略本身带资源(如打开的文件句柄),则应改用 std::unique_ptr 由 Context 独占,或让策略继承 QObject 挂到对象树上——两种所有权模型都成立,关键是想清楚并写进注释。

⑦代码执行流程

以第 ⑤ 节的最小示例为主线,梳理一次完整的策略调用与切换流程:

  1. 创建策略并注入:main 中 std::make_unique<NormalDiscount>() 在堆上创建具体策略对象,通过 Checkout 构造函数传入;std::move 把所有权转移给 Context 的 m_strategy 成员(RAII:离开作用域自动析构,无手动 delete、无泄漏)。
  2. 委托调用:客户端调用 checkout.settle(240.0)。Context 不计算任何折扣,而是执行 m_strategy->calc(price)——这是虚函数调用,运行期通过 vtable 分派到当前对象的真实类型(NormalDiscount::calc),返回 240。
  3. 运行时换策略:学生来结账,调用 checkout.setStrategy(std::make_unique<StudentDiscount>())。函数内部 m_strategy = std::move(s):旧策略(NormalDiscount 对象)被 unique_ptr 析构释放,新策略接管。Context 内部只是换了一个指针,Checkout 类代码零修改。
  4. 再次委托:下一次 settle(240.0) 走新的 vtable,调用 StudentDiscount::calc(240 × 0.85 = 204)。之后 VIP、满减策略同理——同一个 Context、同一个调用点,行为随注入的策略而变。

Qt 示例的流程与之一致,只是「换策略」的触发源从手写调用变成了信号槽:用户操作 QComboBox → 发出 currentIndexChanged 信号 → lambda 槽执行 exporter.setStrategy(...) → 用户点「导出预览」→ exportDocument 委托给新策略渲染。整个流程中,DocumentExporter 对「纯文本 / HTML / Markdown 的区别」一无所知,这正是策略模式的核心价值:把变化关进策略类的笼子里,Context 永远只面对那个稳定的接口。

⑧为什么这样设计

策略模式的每一个设计决策,都对应一条可论证的工程收益:

设计代价(要诚实面对):类数量增加、代码总行数略增、调用多一次虚函数间接跳转(现代 CPU 分支预测下开销可忽略,热点路径可用 std::function 或模板策略进一步优化)。策略模式的本质是用少量的结构冗余,换取长期的演进安全——在算法会频繁变化、需要独立演进的场景里,这笔交易非常划算。

⑨不使用会怎样

把第 ② 节的坏代码继续「演进」两年,你会得到这样的世界:

场景一:switch 野蛮生长,核心函数变成「面条」。产品经理半年内加了 PLUS 会员、企业协议价、618、双 11、新客首单立减…… calcFinalPrice 的 switch 从 4 个 case 长到 20 个 case,还要处理叠加优先级(先会员折扣、再满减、再券),层层嵌套的 if 让任何一个新人都不敢动它——改坏了双 11 的定价,责任谁都担不起,于是大家选择「再加一个 case」,函数继续腐烂。

场景二:分支逻辑复制粘贴,改一处漏三处:

// 算积分:又要按用户类型复制一遍分支
double calcPoints(double amount, UserType type) {
    double base = amount;                       // 1 元 = 1 积分
    if (type == UserType::Vip) base *= 2.0;     // VIP 双倍积分
    if (type == UserType::Student) base *= 1.5; // 学生 1.5 倍
    // ...更多类型来了继续加
    return base;
}

// 算运费:还要再复制一遍「谁包邮」的判断
double shippingFee(double amount, UserType type) {
    if (type == UserType::Vip || type == UserType::Enterprise)
        return 0.0;                             // VIP/企业包邮
    return 10.0;
}

某天「PLUS 会员 8 折」上线,你记得改了 calcFinalPrice,却忘了 calcPoints 里 PLUS 该有的 3 倍积分——线上 bug。更糟的是这类 bug 没有编译期保护,只能靠测试撞出来,而分支组合的测试矩阵早已爆炸到没人维护。

场景三:用继承硬扛,子类爆炸。有人试图用继承解决:「VipOrder 继承 Order 覆写计价」。但 EnterpriseVipOrder、StudentVipOrder……类型一多,类数量呈笛卡尔积爆炸(N 种身份 × M 种活动);而且计价规则混在订单类里,订单的序列化、状态流转、持久化代码被迫跟着每个新规则重新编译、重新测试——继承把「算法变化」和「对象生命周期」两条独立的轴焊死在一起。

场景四:枚举值泄漏到整个代码库。UI 层要 if (type == UserType::Vip) 决定显示金色徽章,日志层要判断类型打印不同文案,网络层要根据类型走不同接口——一个本应只属于「计价领域」的概念,污染了所有层。加一个新类型 = 全库搜索所有 switch 点,漏一个就是静默错误。

反例的共同病灶:算法(易变)与流程(稳定)耦合在同一个函数/类里,导致「一处需求、多处修改、N 处隐患」。策略模式正是把这条耦合拆开的手术刀。

⑩何时使用

适合使用(3~5 个典型场景):

  1. 同一行为存在多种算法变体,且需要在运行时切换:压缩(zip/gzip/7z)、加密(AES/SM4/RC4)、排序(按价格/按销量/按评分)、导航路线(驾车/公交/骑行)。这是策略模式最经典的舞台。
  2. 业务规则按「客户类型 / 渠道 / 场景」分化:计费规则(不同套餐计费)、折扣规则、税率计算、运费模板——规则经常新增或调整,且希望互不影响。
  3. 代码中出现一长串按类型枚举的 if-else / switch,且分支体都是「一小段独立算法」:这正是重构信号——把每个分支体提取成策略类,switch 就消失了。
  4. 算法需要被多个 Context 复用:同一个校验算法(如身份证校验)既要在注册流程用、也要在找回密码流程用——封装成策略/独立算法对象后随处可注入。
  5. 希望算法可配置、可插拔(配置驱动 / 插件化 / A-B 实验):策略 + 简单工厂 + 配置字符串,线上热切换算法而不用改业务代码。

不适合使用(2~4 个场景):

  1. 只有一个实现,且看不到第二个变体的影子:为一个永远不会变化的算法引入接口 + 类层次,属于纯浪费(YAGNI)。等第二个变体真的出现时再重构,成本极低。
  2. 策略需要访问 Context 的大量内部状态:如果每个策略都要 Context 暴露七八个 getter 才能工作,接口会越变越宽、耦合不降反升——此时应考虑把算法放进 Context 内部(或改用模板方法/访问者)。
  3. 策略之间共享大量公共逻辑:若 80% 的代码相同、只有 20% 不同,继承(模板方法)比策略更合适,避免每个策略类里重复那 80%。
  4. 超高频调用路径(如每帧、每元素):虚函数间接调用 + 对象分配的开销虽然小,但在纳秒级热路径上仍可能成为瓶颈;此时考虑模板策略(编译期绑定)或直接函数指针。

过度设计提醒:只有两三种简单变体、且几乎不变时,一个 std::function 成员 + 几个 lambda 就够用了——C++17 里策略模式有两种实现档位:轻量档(std::function/函数指针,适合无状态简单算法)与重量档(抽象基类 + 策略对象,适合有状态、多方法、需复用的算法族)。经验法则:从轻量档起步,当出现「策略需要持有状态」「策略有多个相关方法」「同一策略要注入多个 Context」时,再升级到重量档。设计模式是工具箱,不是勋章墙——用解决问题的成本去衡量,而不是用模式数量去炫耀。

⑪与其他模式的区别

对比模式核心意图与策略模式的关键差异
状态模式(State)对象行为随内部状态改变而改变二者结构几乎相同,但意图相反:State 中状态对象自己决定「下一个状态是谁」并自动切换(切换逻辑藏在状态类里),行为变化是自发的;Strategy 中策略之间互不知晓、绝不互相切换,由客户端显式选择。简单记忆:State 是「内部状态驱动」,Strategy 是「外部选择驱动」。
模板方法模式(Template Method)基类定义算法骨架,子类覆写可变步骤TM 用继承 + 覆写(白盒复用,编译期绑定,子类必须继承基类);Strategy 用组合 + 委托(黑盒复用,运行期绑定,可自由替换)。同一个「折扣」需求:TM 把折扣写进 Order 的子类,Strategy 把折扣做成独立对象注入 Order。明天 Day 21 详解。
命令模式(Command)把「请求/动作」封装成对象,支持排队、撤销、日志Command 封装的是一次动作调用(含接收者与参数),关注「发起者与执行者解耦、动作可延迟/可撤销」;Strategy 封装的是一个算法,关注「算法可互换」。Command 常带 execute()/undo(),Strategy 通常只有单一算法方法。
桥接模式(Bridge)把抽象与实现两个维度分离,各自独立演化结构上与 Strategy 几乎一模一样(都是持有接口 + 委托),但 Bridge 是结构型模式,通常在设计期为了两个正交维度(如 形状×绘制API)而搭建,相对固定;Strategy 是行为型模式,在运行期为算法切换而存在。同一个类图,意图不同,归属不同。

一句话辨析:Strategy、State、Bridge 三者类图相似,区别全在意图——Bridge 是「结构上分层」,State 是「行为自动流转」,Strategy 是「算法按需插拔」。面试时先答意图,再谈结构,就不会混淆。

⑫Qt 源码中的体现

Qt 是策略模式的「重度用户」,以下三处最具代表性:

可以总结出一条规律:凡是 Qt 中「以 Q 开头、名字像名词、内部一堆虚函数、通过工厂创建、可被 set 到某个对象上」的类(QStyle、QImageIOHandler、QTextCodec/QStringConverter),八成是策略或与策略近亲。读 Qt 源码时,看到「xxx->yyy() 的 yyy 是虚函数且 xxx 来自工厂」,就可以按策略模式的框架去理解它了。

⑬面试常见问题

Q1:策略模式解决什么问题?它和 if-else 的本质区别是什么?

答案:它把一族可互换的算法各自封装成独立类,使算法变化独立于使用它的客户端。和 if-else 的本质区别不在「能不能实现」,而在变化的影响范围:if-else 把算法内联在流程里,新增算法必须修改已稳定的流程函数(违背 OCP),且分支逻辑会在多个函数间复制;策略模式把每个算法收敛成一个类,新增算法只新增类、流程零修改,还能独立测试、独立复用。一句话:if-else 是「算法绑架流程」,策略是「算法与流程解耦」。

Q2:策略模式和状态模式结构几乎一样,怎么区分?

答案:看「谁决定行为变化、变化如何发生」。状态模式中,对象的行为随内部状态改变,状态对象自己持有下一个状态的引用并自动切换(状态转移是模式的核心,Context 甚至不知道状态在变);策略模式中,策略对象之间完全独立、互不切换,由客户端显式选择并注入策略。类比:状态模式像红绿灯(自动流转),策略模式像打车选车型(乘客手动选)。实际代码里,若策略/状态类里有「把自己换成另一个对象」的逻辑,那是 State;若只在 Context 外部换,那是 Strategy。

Q3:策略模式和模板方法模式都用来处理「算法族」,怎么选?

答案:看算法骨架归谁。若骨架不变、个别步骤变,用模板方法:基类写好骨架(final),把可变步骤声明为虚函数,子类继承并覆写——继承体系、编译期绑定、白盒复用,缺点是子类和基类强耦合、无法运行时换算法。若整个算法作为整体可替换,用策略:算法完整封装进独立对象,Context 通过组合持有并委托——运行期绑定、黑盒复用、自由插拔,缺点是类数量增加。经验法则:想「换整个算法」选 Strategy,想「固定流程、定制步骤」选 Template Method。

Q4:策略对象应该由谁来创建?Context 自己 new 行不行?

答案:推荐由客户端或工厂创建后注入 Context,Context 只负责持有和委托。若 Context 自己 new 具体策略,它就得知道所有策略类的名字——Context 会同时承担「策略选择」职责,且新增策略时 Context 又要改(OCP 破功)。进阶做法:把「根据条件选策略」的逻辑收敛到简单工厂(DiscountFactory::create("vip"))或注册表/配置文件,客户端只跟工厂打交道;这样策略的选择与使用彻底分离,新增策略只动工厂注册处。另外注意所有权:Context 用 unique_ptr 独占(构造/set 时转移所有权)或非拥有指针(策略由外部保证生命周期),二选一并在接口注释里写清楚。

Q5:C++ 实现策略模式有哪些实践要点?

答案:① 抽象基类要有虚析构(virtual ~IStrategy() = default;),否则经基类指针 delete 派生对象是未定义行为;② 优先 std::unique_ptr 管理策略所有权,setStrategy 用 std::move 转移,杜绝裸 new/delete;③ 无状态策略可做成共享单例/静态实例,多个 Context 复用,省去反复分配;④ 若策略只有单个简单操作,可用 std::function 替代抽象基类,配合 lambda 实现轻量策略(但会失去「策略带状态、多方法、可命名」的能力);⑤ 注意线程安全:若策略有内部状态且被多线程共享,要么无状态、要么加锁、要么每线程实例;⑥ 虚函数调用开销在绝大多数场景可忽略,别为「性能」过早放弃清晰的策略结构,先测量再优化。

⑭今日练习

练习:为日志系统实现「输出策略」(建议 20~30 分钟)

需求:你接手了一个 Qt 应用,现有的 Logger 类把日志写进控制台。现在要求支持两种新输出方式:① 写入文件(追加模式,带时间戳行);② 可选的「网络上报」(发送到指定 URL,用 QNetworkAccessManager,可先留空实现)。要求:新增输出方式时,Logger 类与现有调用点一行都不能改;应用运行时能通过下拉框(或配置文件)自由切换输出方式,且同一时刻的日志要能同时走多个策略(提示:这需要组合——可以用一个「聚合策略」持有若干子策略,逐个转发)。

提示:① 先定义抽象接口 ILogSink { virtual void write(LogLevel, const QString &msg) = 0; },把现有控制台输出重构成 ConsoleSink;② Logger 持有 std::vector<std::unique_ptr<ILogSink>> 或单个策略指针 + 一个 CompositeSink,setSink/addSink 负责注入;文件写入用 QFile + QTextStream,记得在析构/关闭时 flush。做完后思考:如果不用策略模式,这个需求会让 Logger 变成什么样?

(不提供完整答案——自己动手 20 分钟,胜过看答案 2 小时。可对照第 ⑤ 节的 Checkout 结构自查。)

⑮今日总结

一句话记忆:策略模式 = 「算法族 + 统一接口 + 运行时插拔」——把会变的算法装进可替换的盒子里,上下文只认接口、不认实现。

代码特征信号(看到这些,就该考虑/认出策略模式):

  1. 一个只有一两个纯虚方法的「算法接口」类(calc/render/sort……),外加多个实现它的「小类」;
  2. Context 类里有一个指向该接口的成员(unique_ptr 或指针),且有 setStrategy 之类的注入方法;
  3. 调用点通过接口间接调用算法——Context 里看不到任何具体算法类的名字;
  4. 新增算法 = 新增一个类,已有类与调用点零修改(OCP 的红利);
  5. 代码里出现按「类型枚举」的长 switch/if-else、且分支体各自独立——这是「用策略模式重构」的入场信号。

明日预告:Day 21 —— 模板方法模式(Template Method Pattern):把不变的东西写死在基类的骨架里,把可变的东西留成子类去填的虚函数——「骨架我来定,步骤你来写」。它与今天的策略模式是一对镜像:一个用继承固定流程,一个用组合替换算法。明天见!