找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2208

积分

0

好友

314

主题
发表于 18 小时前 | 查看: 2| 回复: 0

你是不是也有这种感觉, C++ 用了好多年,用着用着,发现曾经啃过的设计模式很少有用武之地了?

如果你这么想,那恭喜你,你已经把设计模式用到了炉火纯青的地步。C++ 和标准库早就把那些经典的设计模式,给融合同化了。

C++设计模式全景图

现在你也不用画 UML 图,不用背 GoF 名字,但你写的每一行现代 C++,都在践行它们的思想,只是不再喊它的名字而已。

下面我就说说到底在哪些地方用过哪些模式。

一、为何你会无感地使用设计模式?

  1. std::sort(vec.begin(), vec.end(), cmp) 传 lambda,那是 策略模式(Strategy)
  2. for (auto x : v) 遍历容器,背后是 迭代器模式(Iterator)
  3. std::unique_ptr 自动关文件,这是 RAII 思想,也是资源管理的确定性释放,其设计理念与 工厂模式(Factory)资源包装器(Resource Wrapper) 一脉相承。

这些都是语言和标准库替你做了封装,你自然无需再去手动绘制 UML 图。这种对设计模式的无感知集成,恰恰是 C++ 强大抽象能力的体现。

C++标准库中的模式封装

再加上我们日常开发最关心的是实际问题:这功能以后能轻松替换吗?单元测试能方便地 mock 吗?修改这里会不会引起连锁反应?当你的关注点聚焦于这些工程实践时,自然不会再有额外精力去纠结眼前这段代码对应着哪个模式的“学名”。

二、从理论狂热到实践筛选

GoF 提出的 23 种设计模式,相信很多朋友的书架上都有一本经典著作。我也曾抱着那本书,如饥似渴地研读了好些天。

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 中属于创建型模式,初衷是保证一个类只有一个实例,并提供全局访问点
这个模式初期使用较多,但后来我逐渐减少了对其的使用。现在我通常只将其用于真正无状态、且进程级唯一的确切场景。放弃滥用的主要原因在于,单例隐藏了依赖关系,给单元测试带来了困难。在实践中,我找到了两种更清晰的替代方案:

  1. main() 函数中创建一个应用上下文(Context)对象,持有所有服务,并通过参数显式传递。
  2. 使用工厂方法返回 std::shared_ptr,由调用方决定是否需要共享同一实例。
    这样既能保证逻辑上的唯一性,又不牺牲代码的可测试性和架构清晰度。在我看来,单例模式并非不能用,而是应当慎用,能避免则尽量避免。

三、设计模式:工程实践的路标,而非玩具

设计模式不是面试时的八股文,也不是架构师炫技的玩具。

设计模式的工程价值

它是无数前辈在踩过深坑后,为我们留下的宝贵路标。你不需要死记硬背那 23 种模式。以我二十多年的开发经验来看,常用且实用的也就那么几个。它们不花哨,但却能在凌晨三点接到系统告警电话时,让你有可能只修改一个文件就迅速解决问题,这正是设计模式RAII等现代C++思想结合带来的工程价值。

回想一下,在你的项目经历中,是否也曾有过那种 “早知道应该这么设计就好了” 的顿悟时刻?其实,那些高效、整洁的代码背后,往往都闪烁着这些经典设计思想的光芒。更多关于C++编程技巧和工程实践的深入讨论,欢迎在云栈社区与广大开发者一同交流。




上一篇:Linux Shell编程从入门到实战:掌握自动化运维与脚本编写
下一篇:网络工程师必知必会的12类核心工具集:从排障到自动化
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-14 18:38 , Processed in 0.233979 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表