策略模式(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 原则角度审视这段坏代码:
核心矛盾:计价算法是易变的、可独立演进的,结算流程是稳定的。把两者硬揉进同一个函数,等于让稳定的部分跟着易变的部分一起频繁返工。策略模式的做法是:用「组合 + 多态」把易变的算法从稳定的流程中剥离出来,各自封装、各自演化、运行时可替换。
策略模式的本质只有三句话:
通俗地讲:Context 是「老板」,策略是「员工」。老板不亲自干活,他只认「能干这活的员工」这个身份(接口),具体派哪个员工上,随时可以换。老板的日常工作流程(接单、验收、交付)完全不受换人影响。
生活类比 1 —— 手机地图导航:你的目的地(Context 的业务目标)是固定的,但「驾车」「公交」「骑行」「步行」是四种不同的路线算法(策略)。地图 App 的框架不会因为你切换出行方式而重写,它只是把你选择的交通方式对应的算法插进去,然后调用「计算路线」这个统一入口。App 与具体算法互不干扰,算法之间也互不知晓。
生活类比 2 —— 商场收银台:收银流程(扫商品 → 报价 → 收款 → 出小票)是固定的;但「现金」「银行卡」「扫码支付」「数字人民币」是四种支付算法。收银员不需要知道每种支付的内部规则,只要顾客说「扫码」,就调用扫码策略。未来新增一种支付方式,收银台和收银机程序一行都不用改,只需要「注册」一个新的支付策略。
这两个类比揭示了策略模式的灵魂:把「做什么」(稳定的业务骨架)与「怎么做」(易变的算法细节)分离,变化的部分通过接口插拔,稳定部分永不触碰变化部分。
策略模式的结构图非常简洁,只有三个角色 + 一个客户端:
┌──────────────────────┐
│ 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 复用,进一步节省内存。
以第 ② 节的电商折扣为例,用策略模式重构。核心变化: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 本身就是一个策略模式的巨型教科书案例——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 挂到对象树上——两种所有权模型都成立,关键是想清楚并写进注释。
以第 ⑤ 节的最小示例为主线,梳理一次完整的策略调用与切换流程:
main 中 std::make_unique<NormalDiscount>() 在堆上创建具体策略对象,通过 Checkout 构造函数传入;std::move 把所有权转移给 Context 的 m_strategy 成员(RAII:离开作用域自动析构,无手动 delete、无泄漏)。checkout.settle(240.0)。Context 不计算任何折扣,而是执行 m_strategy->calc(price)——这是虚函数调用,运行期通过 vtable 分派到当前对象的真实类型(NormalDiscount::calc),返回 240。checkout.setStrategy(std::make_unique<StudentDiscount>())。函数内部 m_strategy = std::move(s):旧策略(NormalDiscount 对象)被 unique_ptr 析构释放,新策略接管。Context 内部只是换了一个指针,Checkout 类代码零修改。settle(240.0) 走新的 vtable,调用 StudentDiscount::calc(240 × 0.85 = 204)。之后 VIP、满减策略同理——同一个 Context、同一个调用点,行为随注入的策略而变。Qt 示例的流程与之一致,只是「换策略」的触发源从手写调用变成了信号槽:用户操作 QComboBox → 发出 currentIndexChanged 信号 → lambda 槽执行 exporter.setStrategy(...) → 用户点「导出预览」→ exportDocument 委托给新策略渲染。整个流程中,DocumentExporter 对「纯文本 / HTML / Markdown 的区别」一无所知,这正是策略模式的核心价值:把变化关进策略类的笼子里,Context 永远只面对那个稳定的接口。
策略模式的每一个设计决策,都对应一条可论证的工程收益:
Context、其他策略、所有调用点全部零修改。第 ⑤ 节加「满减」时,Checkout 类一个字节都没动,这就是 OCP 的直接证据。Context 只依赖 IDiscountStrategy 这个抽象接口,策略细节向下沉淀到叶子类。高层(结算流程)不再受低层(计价细节)变化的影响。StudentDiscount 就是「学生价算法」)。出 bug 时定位范围缩小到一个类;改需求时影响面也收缩到一个类。assert(StudentDiscount().calc(200) == 170) 一行就是一个用例;测试组合从「全分支矩阵」坍缩为「每策略独立 + 少量集成用例」。factory.create("vip")),业务上常用来做 A/B 实验、灰度开关——线上切换计价/推荐算法只需改配置,不用发版。设计代价(要诚实面对):类数量增加、代码总行数略增、调用多一次虚函数间接跳转(现代 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 个典型场景):
不适合使用(2~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 是策略模式的「重度用户」,以下三处最具代表性:
QStyle 是抽象策略接口,声明了几十个绘制/度量虚函数(drawControl、drawPrimitive、pixelMetric、polish……),即「一套 UI 绘制算法」的完整契约;QFusionStyle、QWindowsStyle、macOS 风格等是具体策略。任何控件绘制时都执行 style()->drawControl(CE_PushButton, ...)——把绘制请求委托给当前策略。QStyleFactory::create("Fusion") + app.setStyle(...) 即可运行时整体换肤,控件代码零改动。这正是「Context(控件)稳定、算法(外观)可换」的教科书级应用。想定制外观?不必改 Qt,继承 QProxyStyle(装饰器 + 策略的结合)覆写个别虚函数即可。QImageIOHandler 插件实现;QImageReader(Context)根据文件头/后缀选择对应 handler,把读取请求委托给它。新增一种图片格式 = 新增一个 handler 插件,读取框架不动——策略 + 插件的经典组合。QStringConverter 抽象了不同字符编码的转换算法(UTF-8、UTF-16、Latin-1……),配合 QStringDecoder/QStringEncoder 使用,按需选择具体编码策略。这是策略思想在基础设施层的体现。可以总结出一条规律:凡是 Qt 中「以 Q 开头、名字像名词、内部一堆虚函数、通过工厂创建、可被 set 到某个对象上」的类(QStyle、QImageIOHandler、QTextCodec/QStringConverter),八成是策略或与策略近亲。读 Qt 源码时,看到「xxx->yyy() 的 yyy 是虚函数且 xxx 来自工厂」,就可以按策略模式的框架去理解它了。
答案:它把一族可互换的算法各自封装成独立类,使算法变化独立于使用它的客户端。和 if-else 的本质区别不在「能不能实现」,而在变化的影响范围:if-else 把算法内联在流程里,新增算法必须修改已稳定的流程函数(违背 OCP),且分支逻辑会在多个函数间复制;策略模式把每个算法收敛成一个类,新增算法只新增类、流程零修改,还能独立测试、独立复用。一句话:if-else 是「算法绑架流程」,策略是「算法与流程解耦」。
答案:看「谁决定行为变化、变化如何发生」。状态模式中,对象的行为随内部状态改变,状态对象自己持有下一个状态的引用并自动切换(状态转移是模式的核心,Context 甚至不知道状态在变);策略模式中,策略对象之间完全独立、互不切换,由客户端显式选择并注入策略。类比:状态模式像红绿灯(自动流转),策略模式像打车选车型(乘客手动选)。实际代码里,若策略/状态类里有「把自己换成另一个对象」的逻辑,那是 State;若只在 Context 外部换,那是 Strategy。
答案:看算法骨架归谁。若骨架不变、个别步骤变,用模板方法:基类写好骨架(final),把可变步骤声明为虚函数,子类继承并覆写——继承体系、编译期绑定、白盒复用,缺点是子类和基类强耦合、无法运行时换算法。若整个算法作为整体可替换,用策略:算法完整封装进独立对象,Context 通过组合持有并委托——运行期绑定、黑盒复用、自由插拔,缺点是类数量增加。经验法则:想「换整个算法」选 Strategy,想「固定流程、定制步骤」选 Template Method。
答案:推荐由客户端或工厂创建后注入 Context,Context 只负责持有和委托。若 Context 自己 new 具体策略,它就得知道所有策略类的名字——Context 会同时承担「策略选择」职责,且新增策略时 Context 又要改(OCP 破功)。进阶做法:把「根据条件选策略」的逻辑收敛到简单工厂(DiscountFactory::create("vip"))或注册表/配置文件,客户端只跟工厂打交道;这样策略的选择与使用彻底分离,新增策略只动工厂注册处。另外注意所有权:Context 用 unique_ptr 独占(构造/set 时转移所有权)或非拥有指针(策略由外部保证生命周期),二选一并在接口注释里写清楚。
答案:① 抽象基类要有虚析构(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 结构自查。)