第二十八章:设计模式综合实战(二)——基于观察者+中介者模式实现聊天室系统

聊到聊天室系统,你可能会想:这不就是个消息转发吗?

嗯,表面上看确实如此。但真正落地时,你会发现一堆麻烦事:用户怎么通知彼此?消息怎么路由?谁来管理用户列表?

我个人习惯用观察者模式处理通知,用中介者模式管理交互。两者一结合,聊天室的骨架就出来了。

28.1 为什么需要两种模式?

先说说观察者模式。它解决的是「一对多」的通知问题。一个用户发消息,其他人都要收到。这不就是典型的观察者吗?

但问题来了——谁当被观察者?谁当观察者?

如果每个用户都观察其他所有用户,那连接数会爆炸。我在项目中遇到过这种情况,用户一多,内存直接飙上去。

所以我们需要中介者。中介者充当中央枢纽,所有消息都经过它转发。用户只跟中介者打交道,不直接耦合。

核心思路:

  • 观察者模式:处理「用户→用户」的通知
  • 中介者模式:解耦用户之间的直接依赖
  • 两者结合:中介者作为被观察者,用户作为观察者

28.2 系统架构设计

先画张图,让你直观感受一下。

聊天中介者 ChatMediator 用户A 观察者 用户B 观察者 用户C 观察者 用户D 观察者 发送消息 发送消息 广播通知 广播通知 发送/注册 广播通知

你看,所有用户只跟中介者相连。用户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