五、观察者模式
在程序设计中,需要为某对象建立一种"通知依赖关系",当该对象的状态发生变化时,通过公告或广播的方式通知一系列相关对象,实现对象之间的联动。但这种一对多的对象依赖关系往往会造成该对象与其相关的一系列对象之间一种特别紧密的耦合关系。
观察者(Observer)模式是一种使用频率较高的行为型设计模式,可以弱化上述的一对多依赖关系,实现对象之间关系的松耦合。观察者模式在工作中往往会在不知不觉中被用到。
5.1 一个遍历问题导致的低效率范例
A 公司开发的单机闯关打斗类游戏因为收益日渐减少,公司举步维艰。看到隔壁 B 公司开发的网络游戏做得风生水起,A 公司的老板决定将这款单机闯关打斗类游戏改造成类似于《魔兽世界》的大型多人角色扮演类游戏(网络游戏),以往单机游戏中的主角变成了游戏中的每个玩家。游戏本身是不收费的,但游戏中的各种道具(例如药品等)是要收费的。这种对项目的改造要投入巨大的人力、物力以及时间成本,项目组又扩充了数名熟悉网络游戏开发的程序员。
某日,策划召集项目组全体人员开会增加新的游戏玩法,核心内容如下:
- 为增加游戏收入,必须实现游戏中玩家群体之间的战争,因为战争会消耗游戏中的各种物资,例如补充生命值或补充魔法值的药品等。为此,引入"家族"概念,玩家可以自由加入某个家族,一个家族最多容纳20个玩家,不同家族的玩家之间可以根据游戏规则在指定时间和地点通过战斗获取利益。
- 家族成员的聊天信息会被家族中的所有其他成员看到,当然,家族其他成员有权屏蔽家族的聊天信息。非本家族的玩家是看不到本家族成员聊天信息的。
策划要求程序率先实现家族成员聊天功能。于是,程序开始了第一版的开发工作,代码如下:
第一版代码实现
cpp
// 玩家父类(以往的战斗者父类)
class Fighter {
public:
Fighter(int tmpID, string tmpName) : m_iPlayerID(tmpID), m_sPlayerName(tmpName) {
m_iFamilyID = -1; // -1表示没加入任何家族
}
virtual ~Fighter() {} // 析构函数
public:
void SetFamilyID(int tmpID) {
m_iFamilyID = tmpID; // 加入家族时设置家族ID
}
private:
int m_iPlayerID; // 玩家ID,全局唯一
string m_sPlayerName; // 玩家名字
int m_iFamilyID; // 家族ID
};
// "战士"类玩家,父类为Fighter
class F_Warrior : public Fighter {
public:
F_Warrior(int tmpID, string tmpName) : Fighter(tmpID, tmpName) {} // 构造函数
};
// "法师"类玩家,父类为Fighter
class F_Mage : public Fighter {
public:
F_Mage(int tmpID, string tmpName) : Fighter(tmpID, tmpName) {} // 构造函数
};
在文件头增加全局玩家列表:
cpp
#include <list>
class Fighter; // 类前向声明
list<Fighter*> g_playerList;
在 Fighter
类中添加聊天功能:
cpp
public:
void SayWords(string tmpContent) {
if (m_iFamilyID != -1) { // 该玩家属于某个家族
for (auto iter = g_playerList.begin(); iter != g_playerList.end(); ++iter) {
if (m_iFamilyID == (*iter)->m_iFamilyID) {
NotifyWords(*iter, tmpContent); // 通知同家族玩家
}
}
}
}
private:
void NotifyWords(Fighter* otherPlayer, string tmpContent) {
cout << " 玩家:" << otherPlayer->m_sPlayerName
<< " 收到了玩家:" << m_sPlayerName
<< " 发送的聊天信息:" << tmpContent << endl;
}
测试代码:
cpp
Fighter* pplayerobj1 = new F_Warrior(10, "张三");
pplayerobj1->SetFamilyID(100);
g_playerList.push_back(pplayerobj1);
Fighter* pplayerobj2 = new F_Warrior(20, "李四");
pplayerobj2->SetFamilyID(100);
g_playerList.push_back(pplayerobj2);
Fighter* pplayerobj3 = new F_Mage(30, "王五");
pplayerobj3->SetFamilyID(100);
g_playerList.push_back(pplayerobj3);
Fighter* pplayerobj4 = new F_Mage(50, "赵六");
pplayerobj4->SetFamilyID(200);
g_playerList.push_back(pplayerobj4);
pplayerobj1->SayWords(" 全族人立即到沼泽地集结,准备进攻!");
执行结果:
玩家:张三 收到了玩家:张三发送的聊天信息:全族人立即到沼泽地集结,准备进攻!
玩家:李四收到了玩家:张三发送的聊天信息:全族人立即到沼泽地集结,准备进攻!
玩家:王五收到了玩家:张三发送的聊天信息:全族人立即到沼泽地集结,准备进攻!
5.2 引入观察者模式
如果把隶属于某个家族的所有玩家收集到一个列表中,当该家族中的某个玩家发出一条聊天信息后,只需遍历该玩家所在家族的列表,效率更高。以下是优化后的代码:
优化后的代码实现
cpp
// 通知器父类
class Notifier {
public:
virtual void addToList(Fighter* player) = 0;
virtual void removeFromList(Fighter* player) = 0;
virtual void notify(Fighter* talker, string tmpContent) = 0;
virtual ~Notifier() {}
};
// 玩家父类
class Fighter {
public:
Fighter(int tmpID, string tmpName) : m_iPlayerID(tmpID), m_sPlayerName(tmpName) {
m_iFamilyID = -1;
}
virtual ~Fighter() {}
public:
void SetFamilyID(int tmpID) { m_iFamilyID = tmpID; }
int GetFamilyID() { return m_iFamilyID; }
void SayWords(string tmpContent, Notifier* notifier) {
notifier->notify(this, tmpContent);
}
virtual void NotifyWords(Fighter* talker, string tmpContent) {
cout << " 玩家:" << m_sPlayerName
<< " 收到了玩家:" << talker->m_sPlayerName
<< " 发送的聊天信息:" << tmpContent << endl;
}
private:
int m_iPlayerID;
string m_sPlayerName;
int m_iFamilyID;
};
// 聊天信息通知器
class TalkNotifier : public Notifier {
public:
void addToList(Fighter* player) override {
int tmpfamilyid = player->GetFamilyID();
if (tmpfamilyid != -1) {
auto iter = m_familyList.find(tmpfamilyid);
if (iter != m_familyList.end()) {
iter->second.push_back(player);
} else {
list<Fighter*> tmpplayerlist;
m_familyList.insert(make_pair(tmpfamilyid, tmpplayerlist));
m_familyList[tmpfamilyid].push_back(player);
}
}
}
void removeFromList(Fighter* player) override {
int tmpfamilyid = player->GetFamilyID();
if (tmpfamilyid != -1) {
auto iter = m_familyList.find(tmpfamilyid);
if (iter != m_familyList.end()) {
m_familyList[tmpfamilyid].remove(player);
}
}
}
void notify(Fighter* talker, string tmpContent) override {
int tmpfamilyid = talker->GetFamilyID();
if (tmpfamilyid != -1) {
auto itermap = m_familyList.find(tmpfamilyid);
if (itermap != m_familyList.end()) {
for (auto iterlist = itermap->second.begin();
iterlist != itermap->second.end(); ++iterlist) {
(*iterlist)->NotifyWords(talker, tmpContent);
}
}
}
}
private:
map<int, list<Fighter*>> m_familyList; // 家族ID -> 玩家列表
};
测试代码
cpp
Fighter* pplayerobj1 = new F_Warrior(10, "张三");
pplayerobj1->SetFamilyID(100);
Fighter* pplayerobj2 = new F_Warrior(20, "李四");
pplayerobj2->SetFamilyID(100);
Fighter* pplayerobj3 = new F_Mage(30, "王五");
pplayerobj3->SetFamilyID(100);
Fighter* pplayerobj4 = new F_Mage(50, "赵六");
pplayerobj4->SetFamilyID(200);
Notifier* ptalknotify = new TalkNotifier();
ptalknotify->addToList(pplayerobj1);
ptalknotify->addToList(pplayerobj2);
ptalknotify->addToList(pplayerobj3);
ptalknotify->addToList(pplayerobj4);
pplayerobj1->SayWords(" 全族人立即到沼泽地集结,准备进攻!", ptalknotify);
cout << " 王五不想再收到家族其他成员的聊天信息了---" << endl;
ptalknotify->removeFromList(pplayerobj3);
pplayerobj2->SayWords(" 请大家听从族长的调遣,前往沼泽地!", ptalknotify);
执行结果:
玩家:张三 收到了玩家:张三发送的聊天信息:全族人立即到沼泽地集结,准备进攻!
玩家:李四收到了玩家:张三发送的聊天信息:全族人立即到沼泽地集结,准备进攻!
玩家:王五 收到了玩家:张三 发送的聊天信息:全族人立即到沼泽地集结,准备进攻!
王五不想再收到家族其他成员的聊天信息了---
玩家:张三收到了玩家:李四发送的聊天信息:请大家听从族长的调遣,前往沼泽地!
玩家:李四收到了玩家:李四发送的聊天信息:请大家听从族长的调遣,前往沼泽地!
观察者模式UML图
图5.1 抽象通知器的UML图
继承 继承 继承 聚合(观察者列表) 聚合(观察者列表) Notifier +addToList() +removeFromList() +notify() TalkNotifier +addToList() +removeFromList() +notify() <<abstract>> Fighter +NotifyWords() F_Warrior +NotifyWords() F_Mage +NotifyWords()
图5.2 非抽象通知器的UML图
继承 继承 聚合(观察者列表) TalkNotifier +addToList() +removeFromList() +notify() <<abstract>> Fighter +NotifyWords() F_Warrior +NotifyWords() F_Mage +NotifyWords()
图5.3 观察者模式角色关系图

5.3 应用联想
-
游戏救援通知
家族成员被攻击时,通知其他成员救援(如《征途》镖车救援)。
-
新闻推送系统
门户网站根据用户兴趣推送新闻(国际/娱乐/美食等)。
-
数据可视化
销售数据变化时,更新饼图、柱状图、折线图。
-
游戏AI攻击
炮楼监视玩家距离,30米内自动攻击。
观察者模式特点:
- 松耦合:依赖抽象而非具体类,符合依赖倒置原则。
- 广播通知:观察目标主动通知观察者,简化一对多设计。
- 开闭原则:新增观察者或观察目标时,无需修改现有代码。