GoF设计模式(C++版)
GoF设计模式(C++版)
GoF设计模式,全称是 Gang of Four(四人组)设计模式,指的是由 Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides 四位作者(合称“GoF”)在1994年出版的经典著作《设计模式:可复用面向对象软件的基础》中总结的 23种经典模式。这23种模式是面向对象设计的基石,至今仍是架构设计和代码审查中的通用“词汇”。为了更直观地掌握,我将它们按三大类型展开,并特别标注了在C++现代实践中的差异。
1 创建型模式(5种):管对象怎么产生
解决对象创建过程种的耦合问题,将创建与使用分离。
| 模式名称 | 核心意图 | C++现代实际提示 |
| 单例(Singleton) | 确保一个类只有一个实例,并提供全局访问点。 | C++11起可以使用static局部变量实现线程安全,需要注意禁用拷贝构造。 |
| 工厂方法(Factory Method) | 父类定义创建对象的接口,让子类决定实例化哪个具体类。 | 常返回std::unique_prt<T>来明确所有权。 |
| 抽象工厂(Abstract Factory) | 创建一系列相关或依赖的对象族,无需指定具体类。 | 常用于切换不同平台或数据库,配合依赖注入。 |
| 建造者(Builder) | 分步骤构造复杂对象(如含有大量可选参数的类) | 结合链式调用(return *this)非常优雅,可用于构建不可变对象。 |
| 原型(Prototype) | 通过拷贝现有对象来创建新对象,而不是通过构造函数。 | 定义虚clone()方法,返回std::unique_prt,利用拷贝构造实现。 |
2 结构型模式(7种):管类/对象如何搭建
解决类或对象的组合关系,确保“大结构”灵活高效。
| 模式名称 | 核心意图 | C++现代实践提示 |
| 适配器 (Adapter) | 将一个类的接口转换成客户期望的另一个接口,解决接口不兼容。 | 可用 std::function 或 lambda 包装不同函数,实现更轻量的适配。 |
| 桥接 (Bridge) | 将抽象部分与实现部分分离,使它们可以独立变化。 | 常用于跨平台GUI或驱动开发,避免继承爆炸。 |
| 组合 (Composite) | 将对象组合成树形结构以表示“部分-整体”的层次结构。 | 容器和叶子节点定义统一接口,需注意生命周期管理(用 shared_ptr)。 |
| 装饰器 (Decorator) | 动态地给对象添加额外的职责,比生成子类更灵活。 | C++中常通过继承基类并持有基类指针实现,注意析构函数设为虚函数。 |
| 外观 (Facade) | 为子系统中的一组接口提供一个统一的简化入口。 | 最常用的模式之一,将复杂库封装为一个简洁的类。 |
| 享元 (Flyweight) | 共享大量细粒度对象,节省内存(如游戏中的粒子、字体)。 | 配合对象池和const成员使用,必须确保对象不可变。 |
| 代理 (Proxy) | 为另一个对象提供替身或占位符以控制对它的访问(延迟加载、权限校验)。 | 可使用 std::shared_ptr 实现简单的智能指针代理。 |
3 行为型模式(11种):管对象间如何交互
解决对象间如何协作、分配职责已经算法的封装。
| 模式名称 | 核心意图 | C++现代实践提示 |
| 策略 (Strategy) | 定义一系列算法,封装起来,使它们可以互相替换。 | 强烈建议用 std::function + Lambda 替代继承,代码极简。 |
| 观察者 (Observer) | 定义对象间的一对多依赖,当一个对象改变状态,所有依赖者得到通知。 | C++中注意生命周期,常用 std::weak_ptr 防止悬垂指针,或使用信号槽库。 |
| 模板方法 (Template Method) | 在父类中定义算法骨架,将某些步骤延迟到子类实现。 | C++中直接用虚函数实现,注意使用 final 防止过度重写。 |
| 状态 (State) | 允许对象在内部状态改变时改变其行为(看起来像换了类)。 | 与策略模式结构极其相似,区别在于状态由上下文内部驱动,策略由外部选择。 |
| 责任链 (Chain of Responsibility) | 将请求沿链传递,直到有一个处理者处理它。 | 可用 std::vector 存储处理器,链式遍历。 |
| 命令 (Command) | 将请求封装为对象,从而支持撤销、排队、日志等操作。 | 配合 std::function 可轻松实现,常用于实现撤销(Undo)栈。 |
| 迭代器 (Iterator) | 提供一种方法顺序访问聚合对象中的元素,而不暴露内部表示。 | STL容器已内置,只需为自定义容器编写迭代器即可。 |
| 中介者 (Mediator) | 用一个中介对象来封装一系列对象之间的交互,降低网状耦合。 | 适合UI控件间的交互,将多对多变成一对多。 |
| 备忘录 (Memento) | 在不破坏封装的前提下,捕获并外部化一个对象的内部状态(快照)。 | 常配合序列化(如JSON)实现保存/加载功能。 |
| 解释器 (Interpreter) | 定义语言的文法,并解释该语言中的句子(如正则表达式、DSL)。 | 使用频率极低,复杂时建议用现成的解析库(如Boost.Spirit)。 |
| 访问者 (Visitor) | 表示一个作用于某对象结构中的各元素的操作,在不改变元素类的前提下定义新操作。 | 使用频率中等,C++中需处理双重分派,配合 std::variant 和 std::visit 可实现更现代的变体。 |
4 特别提醒:GoF模式在C++种的演进
四位大师写书时用的是90年代的C++(当时还没有C++11)。在现代C++(C++11\14\17\20)中,很多模式被语言特性“降维打击”了:
- 策略模式:被 std::function + Lambda 取代了类继承。
- 命令模式:被 std::packaged_task 和线程池结合。
- 迭代器模式:直接融入STL,用范围 for 循环即可。
- 单例模式:被 static 局部变量的线程安全初始化取代。
5 策略模式
策略模式是一种行为型设计模式,它定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户,从而实现了开闭原则——对扩展开放,对修改关闭。
在 C++ 中,策略模式有多种实现方式,从经典的面向对象接口继承,到现代 C++ 的函数式风格(std::function、Lambda)。
下面从概念、结构、实现、示例、优缺点和应用场景等方面详细讲解。
5.1 结构
策略模式包含三个核心角色:
-
Strategy(抽象策略):定义所有支持的算法的公共接口。通常是一个抽象基类,包含一个纯虚函数。
-
ConcreteStrategy(具体策略):实现
Strategy接口,提供具体的算法实现。 -
Context(上下文):持有一个对
Strategy对象的引用,负责维护策略对象的生命周期,并委托给策略对象执行算法。上下文本身不关心具体策略,只通过抽象接口调用。
5.2 经典C++实现(基于继承和多态)
- 定义抽象策略接口
#include <iostream> #include <memory> // 抽象策略:支付方式 class PaymentStrategy { public: virtual ~PaymentStrategy() = default; virtual void pay(int amount) const = 0; };
- 具体策略类
// 具体策略:信用卡支付 class CreditCardPayment : public PaymentStrategy { public: void pay(int amount) const override { std::cout << "Paid " << amount << " using Credit Card." << std::endl; } }; // 具体策略:支付宝支付 class AlipayPayment : public PaymentStrategy { public: void pay(int amount) const override { std::cout << "Paid " << amount << " using Alipay." << std::endl; } }; // 具体策略:微信支付 class WechatPayment : public PaymentStrategy { public: void pay(int amount) const override { std::cout << "Paid " << amount << " using WeChat Pay." << std::endl; } };
- 上下文类
// 上下文:订单类,包含支付策略 class Order { private: std::unique_ptr<PaymentStrategy> strategy_; // 持有策略对象 public: // 构造时注入策略(也可提供setter动态切换) explicit Order(std::unique_ptr<PaymentStrategy> strategy) : strategy_(std::move(strategy)) {} // 设置新策略(运行时切换) void setStrategy(std::unique_ptr<PaymentStrategy> strategy) { strategy_ = std::move(strategy); } void checkout(int amount) const { if (strategy_) { strategy_->pay(amount); } else { std::cout << "No payment strategy set!" << std::endl; } } };
- 客户端使用
int main() { // 创建订单,使用信用卡支付 Order order(std::make_unique<CreditCardPayment>()); order.checkout(100); // 切换策略为支付宝 order.setStrategy(std::make_unique<AlipayPayment>()); order.checkout(200); // 切换策略为微信 order.setStrategy(std::make_unique<WechatPayment>()); order.checkout(300); return 0; }
输出:
Paid 100 using Credit Card. Paid 200 using Alipay. Paid 300 using WeChat Pay.
5.3 现代C++实现(使用std::function和Lambda)
在 C++11 及以后,我们可以利用 std::function 和 Lambda 表达式,将策略实现为函数对象,无需定义继承层次。这种方式更轻量,适合策略逻辑简单且不需要复杂状态的场景。
#include <iostream> #include <functional> #include <memory> // 上下文直接持有 std::function class Order { private: std::function<void(int)> payment_func_; // 策略函数 public: // 构造函数接受任何可调用对象 explicit Order(std::function<void(int)> payment_func) : payment_func_(std::move(payment_func)) {} void setPaymentStrategy(std::function<void(int)> payment_func) { payment_func_ = std::move(payment_func); } void checkout(int amount) const { if (payment_func_) { payment_func_(amount); } else { std::cout << "No payment strategy set!" << std::endl; } } }; int main() { // 使用 Lambda 定义具体策略 auto credit_pay = [](int amount) { std::cout << "Paid " << amount << " using Credit Card." << std::endl; }; auto alipay_pay = [](int amount) { std::cout << "Paid " << amount << " using Alipay." << std::endl; }; auto wechat_pay = [](int amount) { std::cout << "Paid " << amount << " using WeChat Pay." << std::endl; }; Order order(credit_pay); order.checkout(100); order.setPaymentStrategy(alipay_pay); order.checkout(200); order.setPaymentStrategy(wechat_pay); order.checkout(300); return 0; }
这种方式的优点是代码更简洁、类型更灵活,甚至可以使用普通函数、Lambda 或 bind 表达式。缺点是丢失了策略类的类型信息,且不利于策略内部状态管理(除非通过捕获引用或使用 std::shared_ptr 管理状态对象)。
5.4 优缺点
优点
-
开闭原则:新增策略无需修改上下文,只需添加新的策略类。
-
避免多重条件语句(如
if-else或switch),提高代码可读性和可维护性。 -
运行时切换:策略可以在程序运行过程中动态更换。
-
算法复用:不同上下文可以共享相同的策略对象。
缺点
-
类数量增加:每个策略都需要一个具体类(或函数),可能增加系统复杂度。
-
客户端必须了解策略差异:客户需要知道有哪些策略,以及它们的不同适用场景。
-
通信开销:上下文与策略之间可能需要进行参数传递,如果策略需要上下文的数据,可能引入耦合。
5.5 使用场景
-
系统需要动态地在几种算法中选择一种。
-
算法逻辑复杂,且经常变化,需要独立封装,便于测试和维护。
-
避免使用庞大的条件判断语句来选择不同行为。
-
对同一个对象,需要在不同场景下使用不同的处理方式(如支付、排序、压缩、加密等)。
浙公网安备 33010602011771号