你是不是也有这种感觉, C++ 用了好多年,用着用着,发现曾经啃过的设计模式很少有用武之地了?
如果你这么想,那恭喜你,你已经把设计模式用到了炉火纯青的地步。C++ 和标准库早就把那些经典的设计模式,给融合同化了。

现在你也不用画 UML 图,不用背 GoF 名字,但你写的每一行现代 C++,都在践行它们的思想,只是不再喊它的名字而已。
下面我就说说到底在哪些地方用过哪些模式。
一、为何你会无感地使用设计模式?
std::sort(vec.begin(), vec.end(), cmp) 传 lambda,那是 策略模式(Strategy)。
for (auto x : v) 遍历容器,背后是 迭代器模式(Iterator)。
std::unique_ptr 自动关文件,这是 RAII 思想,也是资源管理的确定性释放,其设计理念与 工厂模式(Factory) 或 资源包装器(Resource Wrapper) 一脉相承。
这些都是语言和标准库替你做了封装,你自然无需再去手动绘制 UML 图。这种对设计模式的无感知集成,恰恰是 C++ 强大抽象能力的体现。

再加上我们日常开发最关心的是实际问题:这功能以后能轻松替换吗?单元测试能方便地 mock 吗?修改这里会不会引起连锁反应?当你的关注点聚焦于这些工程实践时,自然不会再有额外精力去纠结眼前这段代码对应着哪个模式的“学名”。
二、从理论狂热到实践筛选
GoF 提出的 23 种设计模式,相信很多朋友的书架上都有一本经典著作。我也曾抱着那本书,如饥似渴地研读了好些天。

GoF 的 23 种设计模式分为三类:
- 创建型(5种):Factory Method、Abstract Factory、Builder、Prototype、Singleton
- 结构型(7种):Adapter、Bridge、Composite、Decorator、Facade、Flyweight、Proxy
- 行为型(11种):Observer、Strategy、Command、State、Template Method、Iterator、Chain of Responsibility、Visitor、Mediator、Memento、Interpreter
听起来琳琅满目,但在真实的生产环境中,被高频使用的其实只有下面几个核心模式。
1、策略模式(Strategy)
需求场景:今天客户要求用 LZ4 压缩算法,明天强制换成 ZSTD,后天测试环境想直接把压缩功能关掉。
如果写一堆 if (algo == "zstd") 的硬编码,反复修改不仅繁琐,还极易遗漏。更好的做法是使用 std::function 来封装算法:
using CompressFunc = std::function<std::vector(std::span)>;
CompressFunc make_compressor(CompressionType type) {
switch (type) {
case ZSTD: return zstd_compress;
case LZ4: return lz4_compress;
default: return [](auto data) {
return std::vector(data.begin(), data.end());
};
}
}
新增算法要求时,只需增加一个 case。主流程的代码纹丝不动,完美符合开闭原则。
2、工厂方法(Factory Method)
场景:程序需要在 Windows 环境连接 MySQL,在 Linux 环境连接 PostgreSQL。不能让每个模块自己去 new 具体的数据库客户端。
解决方案是写一个工厂函数:
std::unique_ptr create_db(const Config& cfg) {
if (cfg.type == "mysql") {
return std::make_unique(cfg.host, cfg.port);
} else if (cfg.type == "postgres") {
return std::make_unique(cfg.uri);
}
throw std::invalid_argument("unknown db type");
}
如果需要多个模块共享同一个数据库连接,只需将返回值改为 std::shared_ptr,便能轻松管理资源的生命周期。
3、观察者模式(Observer)
场景:实现配置热更新。最初采用轮询文件的方式,效率低下。后来改为配置一旦变更,自动通知所有依赖模块刷新。
核心实现就是一个回调列表:
class EventSource {
std::vector> listeners_;
public:
void add_listener(std::function cb) {
listeners_.push_back(std::move(cb));
}
void notify(State s) {
auto copy = listeners_; // 防止遍历时监听器把自己移除
for (auto& cb : copy) cb(s);
}
};
日志系统、性能监控、状态同步都可以基于此模式构建。事件的发布者完全无需知道具体有哪些订阅者。
4、适配器模式(Adapter)
场景:需要接入 AWS S3、阿里云 OSS、腾讯云 COS 三家云存储,它们的 SDK 接口各异。如果业务代码直接调用,更换厂商几乎等于重写。
解决方案是定义一个统一的抽象接口:
class CloudStorage {
public:
virtual ~CloudStorage() = default;
virtual bool put(std::string_view key, std::string_view data) = 0;
};
class AWSS3Adapter : public CloudStorage { /* 内部调用 AWS SDK */ };
class AliOSSAdapter : public CloudStorage { /* 内部调用阿里云 SDK */ };
业务代码只依赖 CloudStorage 接口,底层实现的更换被隔离在适配器内部。
5、装饰器模式(Decorator)
场景:RPC 调用需要增加重试、超时控制、链路追踪等功能。如果全部塞进核心业务逻辑,函数会变得臃肿不堪。
通过装饰器层层包裹,可以动态添加功能:
class RetryDecorator : public Service {
std::unique_ptr inner_;
int max_retries_;
public:
Result work(Request req) override {
for (int i = 0; i < max_retries_; ++i) {
auto r = inner_->work(req);
if (r.ok()) return r;
std::this_thread::sleep_for(100ms);
}
return Result::error("max retries");
}
};
使用时像套娃一样组合:
auto svc = std::make_unique();
svc = std::make_unique(std::move(svc));
svc = std::make_unique(std::move(svc), 3);
这种方式比使用继承链灵活得多,也更容易维护。
6、命令模式(Command)
场景:线程池任务需要支持异步执行、队列管理、失败重试。早期直接传递函数指针或 lambda,但缺乏对任务本身的抽象和管理能力。
std::function 本身就是一个轻量级的命令对象:
class ThreadPool {
public:
void enqueue(std::function task) {
// 可以在这里包装任务:添加日志、计时、重试逻辑
tasks_.push(std::move(task));
}
private:
std::queue> tasks_;
};
// 提交任务
thread_pool.enqueue([]{
download_and_process("https://example.com/data");
});
std::function 将“要执行的操作”封装成了一个可存储、可传递、可拷贝的数据对象。任务调度器只需调用 operator(),完全无需关心其具体内容。
7、状态模式(State)
场景:网络连接有多个状态(断开、连接中、已连接、错误等)。最初使用 switch (state),每次修改状态转移逻辑都如履薄冰,容易出错。
为每个状态定义一个类:
class Disconnected : public ConnectionState {
void on_connect(Connection& conn) override {
if (conn.try_connect()) {
conn.set_state(std::make_unique());
}
}
};
当线上出现连接问题时,直接查看当前状态对象是哪个,比在冗长的日志中寻找状态码要快得多,调试效率显著提升。
8、PImpl(桥接模式的 C++ 特化)
场景:对外发布 SDK 库,最忌讳头文件暴露内部复杂的依赖。使用 PImpl(Pointer to Implementation)模式可以完美解决:
// MyClass.h (对外头文件)
class MyClass {
class Impl;
std::unique_ptr p_;
public:
MyClass();
~MyClass();
void do_work();
};
// MyClass.cpp (内部实现文件)
class MyClass::Impl {
HeavyDependency dep_; // 用户看不见这个
};
void MyClass::do_work() { p_->dep_.work(); }
用户包含你的头文件时,完全看不到 HeavyDependency 的存在。当你修改内部实现时,只需要重新编译实现文件,而不必触发依赖此头文件的所有项目重新编译。
9、单例模式(Singleton)
单例模式在 GoF 中属于创建型模式,初衷是保证一个类只有一个实例,并提供全局访问点。
这个模式初期使用较多,但后来我逐渐减少了对其的使用。现在我通常只将其用于真正无状态、且进程级唯一的确切场景。放弃滥用的主要原因在于,单例隐藏了依赖关系,给单元测试带来了困难。在实践中,我找到了两种更清晰的替代方案:
- 在
main() 函数中创建一个应用上下文(Context)对象,持有所有服务,并通过参数显式传递。
- 使用工厂方法返回
std::shared_ptr,由调用方决定是否需要共享同一实例。
这样既能保证逻辑上的唯一性,又不牺牲代码的可测试性和架构清晰度。在我看来,单例模式并非不能用,而是应当慎用,能避免则尽量避免。
三、设计模式:工程实践的路标,而非玩具
设计模式不是面试时的八股文,也不是架构师炫技的玩具。

它是无数前辈在踩过深坑后,为我们留下的宝贵路标。你不需要死记硬背那 23 种模式。以我二十多年的开发经验来看,常用且实用的也就那么几个。它们不花哨,但却能在凌晨三点接到系统告警电话时,让你有可能只修改一个文件就迅速解决问题,这正是设计模式与RAII等现代C++思想结合带来的工程价值。
回想一下,在你的项目经历中,是否也曾有过那种 “早知道应该这么设计就好了” 的顿悟时刻?其实,那些高效、整洁的代码背后,往往都闪烁着这些经典设计思想的光芒。更多关于C++编程技巧和工程实践的深入讨论,欢迎在云栈社区与广大开发者一同交流。