第二十八章:设计模式综合实战(二)——基于观察者+中介者模式实现聊天室系统
聊到聊天室系统,你可能会想:这不就是个消息转发吗?
嗯,表面上看确实如此。但真正落地时,你会发现一堆麻烦事:用户怎么通知彼此?消息怎么路由?谁来管理用户列表?
我个人习惯用观察者模式处理通知,用中介者模式管理交互。两者一结合,聊天室的骨架就出来了。
28.1 为什么需要两种模式?
先说说观察者模式。它解决的是「一对多」的通知问题。一个用户发消息,其他人都要收到。这不就是典型的观察者吗?
但问题来了——谁当被观察者?谁当观察者?
如果每个用户都观察其他所有用户,那连接数会爆炸。我在项目中遇到过这种情况,用户一多,内存直接飙上去。
所以我们需要中介者。中介者充当中央枢纽,所有消息都经过它转发。用户只跟中介者打交道,不直接耦合。
核心思路:
- 观察者模式:处理「用户→用户」的通知
- 中介者模式:解耦用户之间的直接依赖
- 两者结合:中介者作为被观察者,用户作为观察者
28.2 系统架构设计
先画张图,让你直观感受一下。
你看,所有用户只跟中介者相连。用户A发消息,中介者收到后通知其他所有人。这就是观察者模式里的「通知」机制。
28.3 代码实现
直接上代码。我会一步步拆解。
28.3.1 观察者接口
// 观察者基类
class IObserver {
public:
virtual ~IObserver() = default;
virtual void onMessage(const std::string& sender,
const std::string& content) = 0;
};
这个接口很简单。每个用户收到消息时,会调用 onMessage。你想想看,这就是观察者模式的核心——被观察者发生变化时,通知所有观察者。
28.3.2 中介者接口
// 中介者基类
class IMediator {
public:
virtual ~IMediator() = default;
virtual void registerUser(IObserver* user) = 0;
virtual void unregisterUser(IObserver* user) = 0;
virtual void broadcastMessage(const std::string& sender,
const std::string& content) = 0;
};
中介者负责三件事:注册用户、注销用户、广播消息。说白了,它就是聊天室的「交换机」。
28.3.3 具体中介者
#include <vector>
#include <algorithm>
#include <iostream>
class ChatMediator : public IMediator {
private:
std::vector<IObserver*> users_;
public:
void registerUser(IObserver* user) override {
users_.push_back(user);
std::cout << "[系统] 新用户加入,当前在线: "
<< users_.size() << "人\n";
}
void unregisterUser(IObserver* user) override {
auto it = std::find(users_.begin(), users_.end(), user);
if (it != users_.end()) {
users_.erase(it);
std::cout << "[系统] 用户离开,当前在线: "
<< users_.size() << "人\n";
}
}
void broadcastMessage(const std::string& sender,
const std::string& content) override {
for (auto* user : users_) {
// 不给自己发
// 这里简化处理,实际需要区分用户身份
user->onMessage(sender, content);
}
}
};
这里有个细节:广播时要不要给发送者自己发?
我个人习惯是不给发送者发。因为发送者自己知道发了什么,没必要再通知一次。但有些业务场景需要「回显」,那就另当别论了。
28.3.4 具体用户
class ChatUser : public IObserver {
private:
std::string name_;
IMediator* mediator_;
public:
ChatUser(const std::string& name, IMediator* mediator)
: name_(name), mediator_(mediator) {
mediator_->registerUser(this);
}
~ChatUser() {
mediator_->unregisterUser(this);
}
void sendMessage(const std::string& content) {
std::cout << "[" << name_ << "] 发送: " << content << "\n";
mediator_->broadcastMessage(name_, content);
}
void onMessage(const std::string& sender,
const std::string& content) override {
if (sender != name_) { // 不显示自己的消息
std::cout << "[" << name_ << "] 收到来自 "
<< sender << ": " << content << "\n";
}
}
};
注意看构造函数:用户创建时自动注册到中介者。析构时自动注销。这避免了忘记调用的坑。
避坑指南:
我曾经在项目里忘记在析构函数中注销观察者,结果用户退出后,中介者还在广播消息,导致野指针崩溃。后来我强制要求:注册和注销必须成对出现,最好在构造/析构中完成。
28.3.5 使用示例
int main() {
ChatMediator mediator;
ChatUser alice("Alice", &mediator);
ChatUser bob("Bob", &mediator);
ChatUser charlie("Charlie", &mediator);
alice.sendMessage("大家好!");
bob.sendMessage("嗨,Alice!");
charlie.sendMessage("有人今晚打游戏吗?");
return 0;
}
输出结果:
[系统] 新用户加入,当前在线: 1人
[系统] 新用户加入,当前在线: 2人
[系统] 新用户加入,当前在线: 3人
[Alice] 发送: 大家好!
[Bob] 收到来自 Alice: 大家好!
[Charlie] 收到来自 Alice: 大家好!
[Bob] 发送: 嗨,Alice!
[Alice] 收到来自 Bob: 嗨,Alice!
[Charlie] 收到来自 Bob: 嗨,Alice!
[Charlie] 发送: 有人今晚打游戏吗?
[Alice] 收到来自 Charlie: 有人今晚打游戏吗?
[Bob] 收到来自 Charlie: 有人今晚打游戏吗?
28.4 两种模式的职责划分
到这里,你可能有点迷糊:观察者和中介者到底各管什么?
我整理了一张表,一目了然:
| 维度 | 观察者模式 | 中介者模式 |
|---|---|---|
| 核心职责 | 一对多的通知机制 | 多对多的交互解耦 |
| 谁通知谁 | 被观察者→观察者 | 中介者→所有参与者 |
| 数据流向 | 单向推送 | 双向转发 |
| 在本例中的角色 | ChatUser 是观察者,Mediator 是被观察者 | ChatMediator 是中介者,管理用户列表和消息路由 |
| 扩展性 | 增加观察者容易,但被观察者变更影响大 | 增加参与者容易,但中介者可能成为瓶颈 |
说白了,观察者模式解决「怎么通知」,中介者模式解决「谁来通知」。两者配合,各司其职。
28.5 进阶优化
上面的实现能跑,但还有优化空间。我提几个点:
28.5.1 消息过滤
实际聊天室需要私聊、群聊、屏蔽等功能。可以在中介者里加过滤逻辑:
void broadcastMessage(const std::string& sender,
const std::string& content,
const std::string& target = "") override {
for (auto* user : users_) {
if (target.empty() || user->getName() == target) {
user->onMessage(sender, content);
}
}
}
28.5.2 线程安全
聊天室通常是多线程的。用户可能同时发消息,中介者需要加锁:
#include <mutex>
class ThreadSafeChatMediator : public ChatMediator {
private:
std::mutex mtx_;
public:
void broadcastMessage(const std::string& sender,
const std::string& content) override {
std::lock_guard<std::mutex> lock(mtx_);
ChatMediator::broadcastMessage(sender, content);
}
};
注意: 加锁粒度要细。如果锁住整个广播过程,性能会下降。我建议只锁住用户列表的遍历,不要在锁内执行耗时的回调。
28.5.3 用户身份识别
上面的代码用字符串比较来避免给自己发消息。更好的做法是给每个用户分配唯一ID:
class ChatUser : public IObserver {
private:
int id_;
static int nextId_;
// ...
public:
ChatUser(const std::string& name, IMediator* mediator)
: id_(nextId_++), name_(name), mediator_(mediator) {
mediator_->registerUser(this);
}
int getId() const { return id_; }
};
28.6 总结
观察者+中介者这个组合,我用了很多年。它特别适合多对多通信的场景。
你想想看,如果没有中介者,每个用户都要维护一个「好友列表」,还要自己处理通知。代码会变得又臭又长。
有了中介者,所有逻辑集中管理。用户只关心发消息和收消息,其他一概不管。这就是解耦的魅力。
嗯,这一章就到这里。代码你拿去跑跑看,有问题随时调。
核心要点回顾:
- 观察者模式负责通知,中介者模式负责路由
- 中介者作为被观察者,用户作为观察者
- 注册和注销一定要成对出现,推荐在构造/析构中完成
- 多线程环境下注意加锁,但粒度要细
- 实际项目中可以扩展私聊、群聊、消息过滤等功能
公众号:蓝海资料掘金营,微信deep3321