25、STL与设计模式:迭代器模式、适配器模式、策略模式、观察者模式
说实话,很多C++程序员学了STL却不会用设计模式,或者学了设计模式却不知道怎么跟STL结合。其实这两者天生就是一对。STL本身就是设计模式的集大成者——迭代器模式、适配器模式、策略模式,这些在STL里随处可见。
我当年刚接触STL时,只觉得它是一堆容器和算法的集合。后来做项目多了才明白,STL的设计思想就是设计模式的最佳实践。今天咱们就来聊聊STL里藏着的那些设计模式,以及怎么用它们搭建一个事件驱动系统。
25.1 迭代器模式:STL的骨架
迭代器模式,说白了就是提供一种统一的方式来遍历容器,而不暴露容器的内部结构。STL里的迭代器就是最经典的实现。
我习惯把迭代器比作「智能指针」。它知道怎么往前走、怎么取值、怎么比较。不同的容器有不同的迭代器,但用法是一样的。
// 迭代器模式的核心:统一遍历接口
std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<int> lst = {10, 20, 30, 40, 50};
// 同样的写法,遍历不同的容器
auto print = [](const auto& container) {
for (auto it = container.begin(); it != container.end(); ++it) {
std::cout << *it << " ";
}
};
print(vec); // 输出: 1 2 3 4 5
print(lst); // 输出: 10 20 30 40 50
你看,迭代器把容器和算法解耦了。算法只需要知道迭代器的接口,不需要关心底层是数组还是链表。这就是迭代器模式的价值。
25.2 适配器模式:stack/queue的底层秘密
适配器模式,就是把一个接口转换成另一个接口。STL里的stack和queue就是典型的适配器——它们底层用的是deque,但对外暴露的是栈和队列的接口。
为什么会这样?因为deque已经提供了所有需要的操作:push_back、pop_back、push_front、pop_front。stack只需要把deque的接口「包装」一下,只暴露push/pop/top,就变成了栈。
// stack的底层实现(简化版)
template <typename T, typename Container = std::deque<T>>
class stack {
public:
void push(const T& value) { c.push_back(value); }
void pop() { c.pop_back(); }
T& top() { return c.back(); }
bool empty() const { return c.empty(); }
private:
Container c; // 默认用deque
};
queue也是一样的道理。它把deque的push_back和pop_front包装成push和pop。这就是适配器模式——不改变底层实现,只改变接口。
25.3 策略模式:函数对象与lambda
策略模式,就是把算法封装成对象,让算法可以互相替换。STL里的函数对象(functor)和lambda就是策略模式的体现。
你想想看,std::sort的第三个参数就是一个策略——你想怎么排序?升序、降序、还是按某个字段排序?这个策略可以随时换。
// 策略模式:不同的排序策略
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};
// 策略1:升序
std::sort(data.begin(), data.end(), std::less<int>());
// 策略2:降序
std::sort(data.begin(), data.end(), std::greater<int>());
// 策略3:自定义策略(按绝对值排序)
std::sort(data.begin(), data.end(),
[](int a, int b) { return std::abs(a) < std::abs(b); });
我建议你在项目中多用函数对象而不是普通函数。为什么?因为函数对象可以保存状态,可以内联优化,性能更好。lambda就是函数对象的语法糖,本质上是一样的。
25.4 观察者模式:signal/slot模拟
观察者模式,就是当一个对象状态变化时,自动通知所有依赖它的对象。STL本身没有直接提供观察者模式,但我们可以用std::function和std::vector来模拟。
我记得在一个UI框架项目中,需要实现按钮点击事件。当时没有用Qt的signal/slot,而是自己用STL搭了一个轻量级的观察者系统。
// 用STL模拟观察者模式
class EventEmitter {
public:
// 注册观察者
template <typename Func>
void connect(Func&& f) {
slots_.push_back(std::forward<Func>(f));
}
// 触发事件
void emit(int data) {
for (auto& slot : slots_) {
slot(data);
}
}
private:
std::vector<std::function<void(int)>> slots_;
};
// 使用
EventEmitter emitter;
emitter.connect([](int x) { std::cout << "观察者1: " << x << "\n"; });
emitter.connect([](int x) { std::cout << "观察者2: " << x * 2 << "\n"; });
emitter.emit(42); // 两个观察者都会被通知
25.5 实战:事件驱动系统
好了,前面讲了这么多模式,咱们来实战一把。用STL搭建一个简单的事件驱动系统,把迭代器、适配器、策略、观察者都用上。
这个系统的核心思想是:事件源产生事件,事件经过管道(适配器)处理,最终分发给观察者。不同的处理策略可以随时切换。
// 事件驱动系统框架
class EventSystem {
public:
using EventHandler = std::function<void(const std::string&)>;
// 注册事件处理器(观察者模式)
void on(const std::string& event, EventHandler handler) {
handlers_[event].push_back(handler);
}
// 触发事件
void emit(const std::string& event, const std::string& data) {
auto it = handlers_.find(event);
if (it != handlers_.end()) {
for (auto& handler : it->second) {
handler(data);
}
}
}
// 事件管道(适配器模式)
template <typename Func>
void pipe(const std::string& event, Func transform) {
auto old_handler = handlers_[event].back();
handlers_[event].back() = [transform, old_handler](const std::string& data) {
old_handler(transform(data));
};
}
private:
std::unordered_map<std::string, std::vector<EventHandler>> handlers_;
};
// 使用示例
EventSystem sys;
// 注册事件
sys.on("click", [](const std::string& msg) {
std::cout << "点击事件: " << msg << "\n";
});
// 添加管道:把消息转成大写(策略模式)
sys.pipe("click", [](const std::string& s) {
std::string result = s;
std::transform(result.begin(), result.end(), result.begin(), ::toupper);
return result;
});
// 触发事件
sys.emit("click", "hello world"); // 输出: 点击事件: HELLO WORLD
这个系统虽然简单,但已经包含了观察者模式(事件分发)、适配器模式(管道转换)、策略模式(可替换的转换函数)。迭代器模式则体现在std::transform和容器的遍历上。
25.6 知识体系总览
下面这张图把本章的知识点串起来了。你可以看到STL设计模式之间的关联,以及它们如何共同构成事件驱动系统。
这张图展示了四个设计模式如何独立存在,又如何在事件驱动系统中协同工作。迭代器负责数据遍历,适配器负责接口转换,策略负责算法替换,观察者负责事件通知。把它们组合起来,就是一个灵活可扩展的系统。
公众号:蓝海资料掘金营,微信deep3321