设计模式在C++中的应用:单例、工厂、观察者与策略

设计模式这东西,说白了就是前人踩坑踩出来的「最佳实践」。我早年写C++时,总觉得模式是花架子,直到接手一个多线程日志系统——那崩溃日志打印得比业务日志还勤快。嗯,从那以后我老实了。

今天咱们聊四个最常用的模式:单例、工厂、观察者、策略。每个我都会结合C++的特性,讲讲线程安全、内存管理这些实战中绕不开的坑。

1. 单例模式:线程安全的正确姿势

单例模式,保证一个类只有一个实例。听起来简单,但多线程环境下,稍不注意就给你new出两个对象来。

经典懒汉式(别这么写)

class Singleton {
public:
    static Singleton* getInstance() {
        if (instance_ == nullptr) {
            instance_ = new Singleton();
        }
        return instance_;
    }
private:
    static Singleton* instance_;
    Singleton() {}
};

这段代码,单线程没问题。多线程下,两个线程同时进入if判断,就会创建两个实例。我当年第一次写服务器时就这么干的,结果线上日志文件被两个实例抢着写,乱成一锅粥。

双检锁 + 内存屏障(C++11起靠谱)

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

C++11保证了局部静态变量的初始化是线程安全的。编译器会帮你加锁,你只管用。我个人习惯用这种方式,简单、高效、不出错。

注意: 别自己手写双检锁加volatile。C++11之前的内存模型不支持,写了也是白写。老老实实用局部静态变量。

2. 工厂模式:把对象的创建封装起来

工厂模式的核心思想:不直接new对象,而是通过一个工厂方法来创建。这样,当需要新增产品时,不用改调用方的代码。

简单工厂

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"; }
};

Product* createProduct(ProductType type) {
    switch (type) {
        case ProductType::A: return new ProductA();
        case ProductType::B: return new ProductB();
        default: return nullptr;
    }
}

简单工厂的缺点很明显:每加一种产品,就要改switch。违反了开闭原则。我在项目中更常用工厂方法模式——把工厂也抽象成接口。

工厂方法模式

class Factory {
public:
    virtual Product* create() = 0;
    virtual ~Factory() = default;
};

class FactoryA : public Factory {
public:
    Product* create() override { return new ProductA(); }
};

class FactoryB : public Factory {
public:
    Product* create() override { return new ProductB(); }
};

这样,新增产品时,只需要新增一个工厂子类,不用改现有代码。你想想看,如果产品有几十种,改switch得多痛苦。

小技巧: 配合std::unique_ptr使用,避免手动delete。工厂返回std::unique_ptr<Product>,调用方不用操心内存。

3. 观察者模式:一对多的通知机制

观察者模式,也叫发布-订阅。一个对象状态变化,通知所有依赖它的对象。GUI事件、消息队列、股票行情推送,都是这个套路。

基本实现

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

class Subject {
public:
    void attach(Observer* obs) { observers_.push_back(obs); }
    void detach(Observer* obs) { /* 从列表中移除 */ }
    void notify(const std::string& msg) {
        for (auto* obs : observers_) {
            obs->update(msg);
        }
    }
private:
    std::vector<Observer*> observers_;
};

这里有个坑:如果观察者在update中把自己从列表中移除了,迭代器就失效了。我曾经在金融交易系统中遇到过,一个观察者收到行情后取消订阅,结果下一个观察者访问了野指针,程序直接崩了。

解决方案:std::weak_ptr管理观察者,或者在通知前先拷贝一份观察者列表。

现代C++改进

std::function代替抽象基类,更灵活:

class EventBus {
public:
    using Handler = std::function<void(const std::string&)>;
    void subscribe(Handler handler) { handlers_.push_back(handler); }
    void publish(const std::string& msg) {
        for (auto& h : handlers_) {
            h(msg);
        }
    }
private:
    std::vector<Handler> handlers_;
};

这样,观察者可以是lambda、函数对象、成员函数,不用继承任何基类。我个人很喜欢这种设计,代码更轻量。

4. 策略模式:把算法抽出来

策略模式,把不同的算法封装成独立的类,让它们可以互相替换。比如排序算法、压缩算法、支付方式。

经典实现

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 MultiplyStrategy : public Strategy {
public:
    int execute(int a, int b) override { return a * b; }
};

class Context {
public:
    void setStrategy(Strategy* s) { strategy_ = s; }
    int doOperation(int a, int b) { return strategy_->execute(a, b); }
private:
    Strategy* strategy_;
};

使用时:

Context ctx;
ctx.setStrategy(new AddStrategy());
std::cout << ctx.doOperation(3, 4); // 7
ctx.setStrategy(new MultiplyStrategy());
std::cout << ctx.doOperation(3, 4); // 12

策略模式的好处是,新增算法不用改Context的代码。坏处是,如果策略很少变化,用if-else反而更简单。别为了用模式而用模式。

现代替代方案: 直接用std::function作为策略。比如Context里存一个std::function<int(int,int)>,赋值时传lambda。代码量少一半。

知识体系总览

下面这张图,把这四种模式的核心逻辑串起来了。你一看就明白它们各自解决什么问题。

设计模式在C++中的应用 单例模式 保证一个类只有一个实例 线程安全:局部静态变量 避免双检锁手写 工厂模式 封装对象创建过程 工厂方法:开闭原则 配合unique_ptr使用 观察者模式 一对多通知机制 迭代器失效风险 可用std::function替代 策略模式 算法封装与替换 避免过度设计 可用lambda替代 核心要点总结 • 单例模式:C++11起用局部静态变量,线程安全且简单 • 工厂模式:工厂方法比简单工厂更符合开闭原则,配合智能指针使用 • 观察者模式:注意迭代器失效,现代C++可用std::function简化 • 策略模式:把算法抽出来,但别过度设计,简单场景用if-else或lambda • 共同原则:优先用现代C++特性(智能指针、lambda、static初始化)

避坑指南

  • 单例的析构顺序: 如果单例A依赖单例B,A析构时B可能已经没了。我建议让单例的生命周期尽量独立,或者用std::shared_ptr管理依赖。
  • 工厂的内存泄漏: 工厂返回裸指针,调用方忘了delete就泄漏了。用std::unique_ptrstd::shared_ptr,让RAII帮你收尸。
  • 观察者的循环依赖: A观察B,B观察A,一个更新触发另一个更新,死循环。加一个递归深度限制,或者用事件队列异步处理。
  • 策略的线程安全: 如果策略对象有状态,多线程下要加锁。最好让策略对象无状态,或者每次创建新实例。

设计模式不是银弹。我见过有人为了用模式,把简单的逻辑拆成七八个类,最后自己都看不懂。记住:模式是工具,不是目的。用之前先问自己——「这个模式真的能降低复杂度吗?」如果答案是否定的,那就别用。


公众号:蓝海资料掘金营,微信deep3321