35、设计模式(C++实现):单例模式(线程安全版本)、工厂模式、观察者模式、策略模式——C++实现要点

设计模式这东西,说白了就是前辈们踩坑踩出来的「最佳实践」。我刚开始用C++写业务逻辑时,总觉得模式是花架子。直到有一次接手一个多线程日志系统,单例没写好,程序跑着跑着就崩了……嗯,从那以后我再也不敢小看模式了。

今天咱们聊聊四个最常用的模式:单例、工厂、观察者、策略。每个我都会给出C++的实现要点,尤其是那些容易翻车的地方。

一、单例模式(线程安全版本)

单例模式,保证一个类只有一个实例。听起来简单,但多线程环境下就麻烦了。

1.1 饿汉式 vs 懒汉式

饿汉式:程序启动时就创建实例。简单,但浪费资源。

懒汉式:第一次使用时才创建。省资源,但线程安全问题多。

我个人习惯用懒汉式,毕竟服务器资源能省则省。但要注意,懒汉式在多线程下会出问题——两个线程同时判断实例为空,然后各自new了一个对象。

1.2 C++11 线程安全实现(推荐)

C++11 之后,我们可以用 std::call_once 或局部静态变量来实现线程安全的单例。我最推荐后者,代码简洁,性能也好。

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;  // C++11 保证线程安全
        return instance;
    }

    // 删除拷贝构造和赋值操作
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    void doSomething() {
        // 业务逻辑
    }

private:
    Singleton() = default;  // 私有构造函数
    ~Singleton() = default;
};

关键点:

  • 局部静态变量在C++11之后是线程安全的——编译器会加锁初始化
  • 构造函数必须私有,防止外部创建
  • 拷贝构造和赋值操作要delete掉

我曾经踩过的坑: 在析构函数里释放了其他单例的资源,导致程序退出时崩溃。原因是单例的析构顺序不确定。我的建议是:尽量让单例不依赖其他单例,或者用智能指针管理生命周期。

二、工厂模式

工厂模式,说白了就是把对象的创建逻辑封装起来。你想想看,如果每个地方都写 new XXX(),一旦构造函数变了,你得改多少地方?

2.1 简单工厂

一个工厂类,根据参数返回不同的产品对象。

enum class ProductType { A, B };

class Product {
public:
    virtual void use() = 0;
    virtual ~Product() = default;
};

class ProductA : public Product {
public:
    void use() override { std::cout << "ProductA\n"; }
};

class ProductB : public Product {
public:
    void use() override { std::cout << "ProductB\n"; }
};

class SimpleFactory {
public:
    static std::unique_ptr<Product> create(ProductType type) {
        switch (type) {
            case ProductType::A: return std::make_unique<ProductA>();
            case ProductType::B: return std::make_unique<ProductB>();
            default: return nullptr;
        }
    }
};

2.2 工厂方法模式

把工厂抽象成接口,每个产品对应一个工厂子类。这样新增产品时不用改已有代码,符合开闭原则。

class Factory {
public:
    virtual std::unique_ptr<Product> create() = 0;
    virtual ~Factory() = default;
};

class FactoryA : public Factory {
public:
    std::unique_ptr<Product> create() override {
        return std::make_unique<ProductA>();
    }
};

class FactoryB : public Factory {
public:
    std::unique_ptr<Product> create() override {
        return std::make_unique<ProductB>();
    }
};

我的经验: 如果产品类型不多且稳定,简单工厂就够了。如果产品经常新增,用工厂方法模式。我在项目中遇到过产品类型从3个膨胀到20个的情况,简单工厂的switch变得又臭又长,后来重构成了工厂方法。

三、观察者模式

观察者模式,也叫发布-订阅模式。一个对象状态变化时,通知所有依赖它的对象。GUI事件、消息队列、数据绑定都用到了它。

3.1 基本实现

class Observer {
public:
    virtual void update(const std::string& message) = 0;
    virtual ~Observer() = default;
};

class Subject {
private:
    std::vector<std::weak_ptr<Observer>> observers;

public:
    void attach(const std::shared_ptr<Observer>& obs) {
        observers.push_back(obs);
    }

    void notify(const std::string& message) {
        for (auto it = observers.begin(); it != observers.end(); ) {
            if (auto obs = it->lock()) {
                obs->update(message);
                ++it;
            } else {
                it = observers.erase(it);  // 移除已销毁的观察者
            }
        }
    }
};

注意:std::weak_ptr 存储观察者,避免循环引用导致内存泄漏。通知时先检查观察者是否还活着。

3.2 线程安全问题

观察者模式在多线程环境下容易出问题——通知过程中有观察者被销毁了怎么办?或者有新的观察者加入?

我建议的做法:

  • 用互斥锁保护观察者列表
  • 通知时先拷贝一份列表,在拷贝上遍历
  • 或者用读写锁,读多写少时性能更好

我曾经遇到的坑: 在通知回调中,观察者又去修改了Subject的观察者列表,导致迭代器失效。解决方案是:通知期间禁止修改列表,或者用递归锁。

四、策略模式

策略模式,把算法封装成独立的策略类,运行时可以切换。说白了就是「把if-else变成多态」。

4.1 经典实现

class Strategy {
public:
    virtual int execute(int a, int b) = 0;
    virtual ~Strategy() = default;
};

class AddStrategy : public Strategy {
public:
    int execute(int a, int b) override { return a + b; }
};

class SubtractStrategy : public Strategy {
public:
    int execute(int a, int b) override { return a - b; }
};

class Context {
private:
    std::unique_ptr<Strategy> strategy;

public:
    void setStrategy(std::unique_ptr<Strategy> s) {
        strategy = std::move(s);
    }

    int doOperation(int a, int b) {
        if (!strategy) throw std::runtime_error("Strategy not set");
        return strategy->execute(a, b);
    }
};

4.2 用std::function简化

如果策略很简单,用函数指针或 std::function 更轻量,不用定义一堆类。

class Context {
private:
    std::function<int(int, int)> strategy;

public:
    void setStrategy(std::function<int(int, int)> s) {
        strategy = std::move(s);
    }

    int doOperation(int a, int b) {
        if (!strategy) throw std::runtime_error("Strategy not set");
        return strategy(a, b);
    }
};

// 使用
Context ctx;
ctx.setStrategy([](int a, int b) { return a + b; });
ctx.setStrategy([](int a, int b) { return a * b; });

我的建议: 策略类有状态(成员变量)时用类实现,无状态时用lambda。我在项目中用策略模式处理过多种压缩算法,每个算法有自己的参数配置,用类实现更清晰。

五、四种模式对比

模式 核心思想 适用场景 C++实现要点
单例 全局唯一实例 日志、配置、线程池 线程安全、禁止拷贝、生命周期管理
工厂 封装对象创建 产品族、配置驱动创建 智能指针返回、开闭原则
观察者 一对多通知 事件系统、消息推送 weak_ptr避免循环引用、线程安全
策略 算法可替换 排序、压缩、支付方式 std::function简化、运行时切换

六、知识体系图

设计模式(C++实现)知识体系 单例模式 工厂模式 观察者模式 策略模式 饿汉式 / 懒汉式 C++11 局部静态变量 std::call_once 简单工厂 工厂方法模式 智能指针 + 开闭原则 std::weak_ptr 存储 线程安全通知 迭代器失效处理 多态策略类 std::function 简化 运行时切换算法 核心原则:封装变化、组合优于继承、 针对接口编程、降低耦合

设计模式不是银弹,但用好了确实能提升代码质量。我个人觉得,初学者不要硬套模式,先理解问题本身,再想哪个模式合适。模式是工具,不是目的。

嗯,今天就聊到这儿。这些模式我在项目中反复用过,每次都有新体会。你写代码时遇到什么模式相关的问题,欢迎交流。

专注资料整理