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简化、运行时切换 |
六、知识体系图
设计模式不是银弹,但用好了确实能提升代码质量。我个人觉得,初学者不要硬套模式,先理解问题本身,再想哪个模式合适。模式是工具,不是目的。
嗯,今天就聊到这儿。这些模式我在项目中反复用过,每次都有新体会。你写代码时遇到什么模式相关的问题,欢迎交流。