44、STL与设计模式实战:工厂模式、单例模式、观察者模式在STL中的应用

说实话,很多C++开发者学了设计模式,也学了STL,但总觉得这两者是割裂的。我早年也有这个困惑——直到我在一个实时交易系统里,被逼着用STL容器去实现观察者模式,才恍然大悟:STL本身就是设计模式的最佳实践土壤。

这一章,咱们就聊聊三个经典模式——工厂模式、单例模式、观察者模式,看看它们怎么跟STL的容器、算法、智能指针结合。说白了,就是让你写出来的代码既有设计感,又有STL的效率和优雅。

工厂模式 + STL:用map管理对象生产

工厂模式的核心,是把对象的创建逻辑封装起来。但传统工厂写出来,往往是一堆if-else或者switch-case。我个人习惯用std::mapstd::unordered_map来替代这些分支判断。

举个例子,假设我们要创建不同类型的日志处理器:

#include <iostream>
#include <memory>
#include <unordered_map>
#include <functional>
#include <string>

class Logger {
public:
    virtual ~Logger() = default;
    virtual void log(const std::string& msg) = 0;
};

class ConsoleLogger : public Logger {
public:
    void log(const std::string& msg) override {
        std::cout << "[Console] " << msg << std::endl;
    }
};

class FileLogger : public Logger {
public:
    void log(const std::string& msg) override {
        std::cout << "[File] " << msg << std::endl; // 实际应写入文件
    }
};

// 工厂类:用unordered_map注册创建函数
class LoggerFactory {
public:
    using Creator = std::function<std::unique_ptr<Logger>()>;

    static LoggerFactory& instance() {
        static LoggerFactory factory;
        return factory;
    }

    void registerLogger(const std::string& type, Creator creator) {
        creators_[type] = std::move(creator);
    }

    std::unique_ptr<Logger> create(const std::string& type) {
        auto it = creators_.find(type);
        if (it != creators_.end()) {
            return it->second();
        }
        return nullptr;
    }

private:
    std::unordered_map<std::string, Creator> creators_;
    LoggerFactory() = default;
};

// 注册产品
auto consoleReg = []() {
    LoggerFactory::instance().registerLogger("console",
        [](){ return std::make_unique<ConsoleLogger>(); });
    return true;
}();

auto fileReg = []() {
    LoggerFactory::instance().registerLogger("file",
        [](){ return std::make_unique<FileLogger>(); });
    return true;
}();

int main() {
    auto logger = LoggerFactory::instance().create("console");
    if (logger) logger->log("Hello from factory!");
    return 0;
}

关键点:用std::function + std::unique_ptr,工厂不再需要硬编码if-else。新增一种日志类型,只需要注册一个lambda,开闭原则完美落地。

我在项目中遇到过一个问题:注册顺序依赖导致崩溃。后来我改用std::map(有序)来保证注册顺序可预测,调试起来舒服多了。

单例模式 + STL:线程安全的局部静态变量

单例模式在C++11之后变得极其简单——局部静态变量的初始化是线程安全的。但如果你需要管理一组单例对象,STL容器就派上用场了。

我个人喜欢用std::map<std::string, std::shared_ptr<T>>来实现一个「单例注册表」:

#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <string>

class SingletonRegistry {
public:
    static SingletonRegistry& getInstance() {
        static SingletonRegistry instance;
        return instance;
    }

    template<typename T>
    std::shared_ptr<T> get(const std::string& key) {
        std::lock_guard<std::mutex> lock(mutex_);
        auto it = registry_.find(key);
        if (it != registry_.end()) {
            return std::static_pointer_cast<T>(it->second);
        }
        auto obj = std::make_shared<T>();
        registry_[key] = obj;
        return obj;
    }

private:
    std::map<std::string, std::shared_ptr<void>> registry_;
    std::mutex mutex_;
    SingletonRegistry() = default;
};

// 使用示例
class ConfigManager {
public:
    void load() { std::cout << "Config loaded." << std::endl; }
};

int main() {
    auto cfg = SingletonRegistry::getInstance().get<ConfigManager>("config");
    cfg->load();
    return 0;
}

注意:用std::shared_ptr<void>做类型擦除,取回时需要static_pointer_cast。我曾经因为类型转换错误导致运行时崩溃,所以建议在get模板里加typeid断言做安全检查。

观察者模式 + STL:用vector/list管理订阅者

观察者模式在GUI、事件系统里太常见了。STL的std::vectorstd::list天然适合存储观察者列表。但有个坑——遍历时如果观察者被移除,迭代器会失效。

我建议用std::vector<std::weak_ptr<Observer>>,配合std::remove_if来清理失效的观察者:

#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
#include <string>

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

class Subject {
public:
    void attach(std::shared_ptr<Observer> obs) {
        observers_.push_back(obs);
    }

    void detach(const std::shared_ptr<Observer>& obs) {
        // 使用erase-remove惯用法移除指定观察者
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [&obs](const std::weak_ptr<Observer>& wp) {
                    auto sp = wp.lock();
                    return !sp || sp == obs;
                }),
            observers_.end());
    }

    void notify(const std::string& event) {
        // 先清理失效的weak_ptr,再通知
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [](const std::weak_ptr<Observer>& wp) {
                    return wp.expired();
                }),
            observers_.end());

        for (auto& wp : observers_) {
            if (auto sp = wp.lock()) {
                sp->update(event);
            }
        }
    }

private:
    std::vector<std::weak_ptr<Observer>> observers_;
};

// 具体观察者
class ConcreteObserver : public Observer {
public:
    explicit ConcreteObserver(const std::string& name) : name_(name) {}
    void update(const std::string& event) override {
        std::cout << name_ << " received: " << event << std::endl;
    }
private:
    std::string name_;
};

int main() {
    Subject subject;
    auto obs1 = std::make_shared<ConcreteObserver>("Observer1");
    auto obs2 = std::make_shared<ConcreteObserver>("Observer2");

    subject.attach(obs1);
    subject.attach(obs2);
    subject.notify("EventA");

    subject.detach(obs1);
    subject.notify("EventB");
    return 0;
}

小技巧:用std::weak_ptr而不是裸指针,可以自动处理观察者被销毁的情况。配合std::remove_if,每次通知前清理一次,既安全又高效。

三种模式与STL的融合关系图

下面这张图展示了三种设计模式与STL核心组件的对应关系,方便你快速回顾:

设计模式与STL融合关系图 工厂模式 核心容器 std::unordered_map std::function std::unique_ptr 注册创建函数 避免if-else 开闭原则 map<string, function<T()>> 单例模式 核心容器 std::map std::shared_ptr std::mutex 线程安全初始化 类型擦除注册表 全局唯一访问点 static局部变量 观察者模式 核心容器 std::vector std::weak_ptr std::remove_if 弱引用管理生命周期 erase-remove惯用法 安全遍历与清理 vector<weak_ptr<T>> STL容器 + 智能指针 + 算法 = 设计模式的现代化实现

避坑指南与个人经验

  • 工厂模式:我曾经在注册lambda时捕获了局部变量,导致悬空引用。建议lambda里只捕获必要的智能指针,或者用std::bind绑定参数。
  • 单例模式:别在单例构造函数里调用其他单例——静态初始化顺序未定义。我吃过这个亏,后来改用std::call_once + 显式初始化。
  • 观察者模式:遍历时如果观察者内部触发了detach,迭代器会失效。我的做法是:通知前先拷贝一份观察者列表的shared_ptr副本,遍历副本,原列表加锁保护。

核心思想:STL不是设计模式的替代品,而是实现设计模式的「乐高积木」。你想想看,用std::map做工厂注册表,用std::weak_ptr做观察者生命周期管理,用std::remove_if做清理——这些组合拳,比手写链表、手写线程安全队列不知道高到哪里去了。

嗯,这一章就到这里。记住:设计模式是思想,STL是工具。把思想装进工具里,你的代码才能既优雅又健壮。