【读书笔记】《C++ Software Design》第五章:The Strategy and Command Design Patterns
《C++ Software Design》第五章:The Strategy and Command Design Patterns
本章重点介绍两种“行为型设计模式”:策略模式(Strategy Pattern)与命令模式(Command Pattern)。核心问题分别是:
- Strategy:隔离如何做(算法实现)
- Command:隔离做什么(执行动作)
深入探讨传统面向对象实现存在的不足,以及如何借助现代 C++ 的特性(如模板、std::function
、值语义)来替代经典写法。
Guideline 19: Use Strategy to Isolate How Things Are Done
目的
我们不希望主类直接控制算法细节,而是通过抽象策略接口将具体算法实现分离出去,使行为具有可配置性、可替换性和可测试性。
Analyzing the Design Issues
问题:如何在不修改主类逻辑的前提下,动态切换算法行为?
示例情境:有一个 Shape
类,它支持不同的“绘图策略”,例如 Vector 和 Raster。
The Strategy Design Pattern Explained
struct DrawingStrategy {virtual void draw() const = 0;virtual ~DrawingStrategy() = default;
};struct VectorDrawing : DrawingStrategy {void draw() const override {std::cout << "Drawing with vector lines.\n";}
};struct RasterDrawing : DrawingStrategy {void draw() const override {std::cout << "Drawing with pixels.\n";}
};class Shape {std::unique_ptr<DrawingStrategy> strategy_;
public:Shape(std::unique_ptr<DrawingStrategy> s) : strategy_(std::move(s)) {}void draw() const { strategy_->draw(); }
};
你可以在运行时将不同策略注入 Shape
对象中。
Analyzing the Shortcomings of the Naive Solution
- 使用虚函数有运行时开销;
- 所有策略都要继承接口类,样板代码多;
- 无法表达“静态策略”:某些行为在编译期就应决定。
Comparison Between Visitor and Strategy
维度 | Strategy | Visitor |
---|---|---|
用于扩展 | 算法的实现 | 操作的种类 |
使用方式 | 客户端选择策略对象 | 类型自身 accept(visitor) 回调 |
增加新操作 | 只需新建策略类 | 必须修改 visitor 接口和实现类 |
动态 vs 静态 | 可动态切换 | 通常是静态绑定 |
Policy-Based Design(模板策略)
借助模板参数实现“编译期策略注入”,零运行时成本:
template<typename DrawingPolicy>
class Shape {
public:void draw() const {DrawingPolicy{}.draw();}
};struct SVGPolicy {void draw() const { std::cout << "SVG drawing\n"; }
};Shape<SVGPolicy> s1;
s1.draw(); // 输出:SVG drawing
优势:
- 零开销(没有虚表);
- 策略间互不耦合;
- 更易于组合多个策略(如日志 + 绘图)。
Guideline 20: Favor Composition over Inheritance
原则解释
继承是紧耦合,破坏封装;组合是一种更灵活的复用方式。
class Engine {
public:virtual void start() = 0;
};class Car {std::unique_ptr<Engine> engine_;
public:Car(std::unique_ptr<Engine> e) : engine_(std::move(e)) {}void drive() {engine_->start();}
};
将“引擎行为”作为组合注入而非通过继承扩展 Car
,支持运行时替换、功能复用。
Guideline 21: Use Command to Isolate What Things Are Done
概念
命令模式将“动作”封装为对象,可用于延迟执行、宏命令、撤销、操作记录等场景。
Command Design Pattern Explained
struct Command {virtual void execute() = 0;virtual ~Command() = default;
};struct PrintCommand : Command {void execute() override {std::cout << "Printing document...\n";}
};class Invoker {std::vector<std::unique_ptr<Command>> queue_;
public:void add(std::unique_ptr<Command> cmd) {queue_.push_back(std::move(cmd));}void run() {for (auto& cmd : queue_) {cmd->execute();}}
};
🆚 Strategy vs Command
特征 | Strategy | Command |
---|---|---|
抽象的目标 | 算法的实现方式 | 动作的执行内容 |
使用方式 | context->run(strategy) | cmd->execute() |
通常搭配对象 | 算法、排序、日志 | 用户事件、GUI、请求调度 |
Shortcomings of Classic Command
- 每种命令都需创建一个类,数量膨胀;
- 动作封装不够灵活,类耦合度高。
Guideline 22: Prefer Value Semantics over Reference Semantics
GoF 风格的问题
经典实现中,大量使用 shared_ptr
或裸指针管理命令/策略对象,容易造成资源管理混乱:
auto cmd = std::make_shared<PrintCommand>();
一旦多个地方持有相同指针,生命周期不易掌控。
现代 C++:值语义策略
struct PrintCommand {void operator()() const {std::cout << "Print\n";}
};std::vector<PrintCommand> cmds = { PrintCommand{} };
- 无需担心内存泄露;
- 容器友好;
- 更安全、易于并发使用。
Value Semantics in std::function
using Command = std::function<void()>;std::vector<Command> cmds;
cmds.push_back([](){ std::cout << "Execute\n"; });
将命令行为直接封装为“值”,即函数对象。
Guideline 23: Prefer a Value-Based Implementation of Strategy and Command
std::function 简化命令和策略
using Strategy = std::function<void()>;class Shape {Strategy draw_;
public:Shape(Strategy s): draw_(std::move(s)) {}void draw() const { draw_(); }
};Shape s([](){ std::cout << "Draw with OpenGL\n"; });
s.draw(); // 输出:Draw with OpenGL
不需要接口类、不需要虚函数,不需要手动管理生命周期。
性能分析
实现方式 | 是否需堆分配 | 是否有虚函数 | 性能 |
---|---|---|---|
传统 Command | 是(智能指针) | 是 | 中等 |
Policy 模板策略 | 否 | 否 | 高(内联优化) |
std::function | 否(小对象优化) | 否 | 中高(取决闭包大小) |
std::function 的限制
- 不支持接口描述:没有显式接口要求;
- 不能静态类型检查(传错参数不会编译失败);
- 拷贝时需注意闭包大小(超过 SBO 时可能 heap 分配)。
小结
模式 | 作用 | 建议实现方式 | 场景适用性 |
---|---|---|---|
Strategy | 隔离“如何做”(算法) | 模板策略 / std::function | 日志、绘图、压缩算法 |
Command | 隔离“做什么”(动作) | std::function | GUI、宏命令、异步请求 |
Value Semantics | 简化资源管理,提高安全性 | 使用函数对象 / RAII | 所有现代 C++ 项目 |
Policy | 零运行时开销的行为注入 | 模板 | 性能敏感、泛型组件设计 |