第五章 观察者(Observer)模式
第五章 观察者(Observer)模式
章节定位与核心目标
本章是行为型设计模式的核心内容之一,承接第 4 章策略模式的 “依赖倒置原则”,通过网络游戏家族聊天功能的代码迭代,解决 “一对多对象依赖” 场景下的高耦合和低效率问题,最终掌握观察者模式的设计思想、结构、角色及实际应用。
第一节 一个遍历问题导致的低效率范例
1.1业务场景背景
A 公司将单机打斗游戏改造为多人网游,策划提出家族聊天功能需求:
- 玩家可加入家族(家族 ID 标识,-1 表示未加入),家族最多容纳 20 人;
- 家族成员聊天信息仅本家族可见,成员可屏蔽信息;
- 非家族成员无法接收该家族聊天内容。
1.2 第一版代码实现逻辑
1.2.1核心类结构与成员
namespace _nmsp1
{
class Fighter; //类前向声明
list<Fighter*> g_playerList;
//玩家父类(以往的战斗者类)
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) //加入家族的时候要设置家族ID
{
m_iFamilyID = tmpID;
}
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;
}
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) {} //构造函数
};
}
1.2.2核心功能:聊天信息的发送与接收
-
发送逻辑(SayWords):
-
先判断玩家是否加入家族(
m_iFamilyID != -1); -
遍历全局玩家列表
g_playerList; -
对比遍历对象与当前玩家的家族 ID,相同则调用
NotifyWords推送信息。void Fighter::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); } } } }
-
-
接收逻辑(NotifyWords):打印接收者、发送者及聊天内容,完成信息展示。
完整代码:
// MyProject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
//公众号:程序员速成 ,内含一辈子都让你感激自己的优质视频教程,欢迎关注
#include <iostream>
#include <list>
#include <map>
#ifdef _DEBUG //只在Debug(调试)模式下
#ifndef DEBUG_NEW
#define DEBUG_NEW new(_NORMAL_BLOCK,__FILE__,__LINE__) //重新定义new运算符
#define new DEBUG_NEW
#endif
#endif
//#include <boost/type_index.hpp>
using namespace std;
//#pragma warning(disable : 4996)
namespace _nmsp1
{
class Fighter; //类前向声明
list<Fighter*> g_playerList;
//玩家父类(以往的战斗者类)
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) //加入家族的时候要设置家族ID
{
m_iFamilyID = tmpID;
}
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;
}
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) {} //构造函数
};
}
int main()
{
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);//程序退出时检测内存泄漏并显示到“输出”窗口
//创建游戏玩家
_nmsp1::Fighter* pplayerobj1 = new _nmsp1::F_Warrior(10, "张三"); //实际游戏中很多数据取自数据库。
pplayerobj1->SetFamilyID(100); //假设该玩家所在的家族的家族ID是100
_nmsp1::g_playerList.push_back(pplayerobj1); //加入到全局玩家列表中
_nmsp1::Fighter* pplayerobj2 = new _nmsp1::F_Warrior(20, "李四");
pplayerobj2->SetFamilyID(100);
_nmsp1::g_playerList.push_back(pplayerobj2);
_nmsp1::Fighter* pplayerobj3 = new _nmsp1::F_Mage(30, "王五");
pplayerobj3->SetFamilyID(100);
_nmsp1::g_playerList.push_back(pplayerobj3);
_nmsp1::Fighter* pplayerobj4 = new _nmsp1::F_Mage(50, "赵六");
pplayerobj4->SetFamilyID(200); //赵六和前面三人属于两个不同的家族
_nmsp1::g_playerList.push_back(pplayerobj4);
//当某个玩家聊天时,同族人都应该收到该信息
pplayerobj1->SayWords("全族人立即到沼泽地集结,准备进攻!");
//释放资源
delete pplayerobj1;
delete pplayerobj2;
delete pplayerobj3;
delete pplayerobj4;
return 0;
}
1.2.3测试案例与效果
// 创建4个玩家:张三/李四/王五(家族100)、赵六(家族200)
_nmsp1::Fighter* pplayerobj1 = new _nmsp1::F_Warrior(10, "张三");
pplayerobj1->SetFamilyID(100);
_nmsp1::g_playerList.push_back(pplayerobj1);
// 李四、王五、赵六创建逻辑类似,此处省略
pplayerobj1->SayWords("全族人立即到沼泽地集结,准备进攻!");
- 效果:张三、李四、王五能收到信息,赵六无法接收,功能符合需求。
1.3第一版代码的核心缺陷
- 效率极低:若网游有上万个玩家,每次聊天需遍历全服玩家,而非仅本家族(最多 20 人);
- 耦合度高:
Fighter类直接依赖全局变量g_playerList,若列表存储结构变更(如改用 map),需修改Fighter源码; - 扩展性差:实现 “屏蔽家族信息” 需修改
SayWords的遍历逻辑,违反开闭原则; - 职责不单一:
Fighter既负责玩家属性管理,又负责聊天信息的遍历推送,违背单一职责原则。
第二节 引入观察者(Observer)模式
1.2.1优化核心思路
- 分组存储:按家族 ID 分组管理玩家,避免全服遍历;
- 抽象解耦:分离 “通知逻辑” 与 “玩家逻辑”,定义
Notifier抽象类解耦依赖; - 动态管理:支持观察者(玩家)的动态添加 / 移除,实现 “屏蔽信息” 功能。
1.2.2 代码迭代对比(从_nmsp1 到_nmsp2 的关键变更)
| 迭代点 | _nmsp1 实现 | _nmsp2 实现 |
|---|---|---|
| 通知逻辑载体 | 嵌入 Fighter 类,依赖全局列表 | 抽象为 Notifier 类,子类实现具体逻辑 |
| 玩家家族 ID 获取 | 无公开接口 | 新增GetFamilyID公有方法 |
| 聊天发送接口 | SayWords(string),自行遍历 |
SayWords(string, Notifier*),调用通知器 |
| 信息接收接口 | 私有方法NotifyWords,不可扩展 |
公有虚方法NotifyWords,支持子类重写 |
| 玩家存储结构 | 全局 list,无分组 | map<int, list<Fighter*>>,按家族分组 |
(1)抽象通知器(Subject / 观察目标):Notifier 类
定义观察者的添加、移除、通知接口,实现 “依赖倒置”,让观察目标依赖抽象而非具体观察者:
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() {}
};
(2)观察者(Observer):Fighter 类的改造
class Fighter//玩家父类
{
public:
Fighter(int tmpID, string tmpName) :m_iPlayerID(tmpID), m_sPlayerName(tmpName) //构造函数
{
m_iFamilyID = -1; //-1表示没有加入任何家族
}
virtual ~Fighter() {} //析构函数
public:
//加入家族的时候要设置家族ID
void SetFamilyID(int tmpID) { m_iFamilyID = tmpID; }
// 新增:获取家族ID(给通知器提供分组依据)
int GetFamilyID() { return m_iFamilyID; }
public:
// 改造:发送聊天需传入通知器,自身不再处理遍历逻辑
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; //玩家ID,全局唯一
string m_sPlayerName; //玩家名字
int m_iFamilyID; //家族ID
};
(3)具体通知器(ConcreteSubject):TalkNotifier 类
实现Notifier接口,用map按家族 ID 分组存储玩家,完成高效通知:
//聊天信息通知器
class TalkNotifier :public Notifier
{
public:
//将玩家增加到家族列表中来
virtual void addToList(Fighter* player)
{
int tmpfamilyid = player->GetFamilyID();
if (tmpfamilyid != -1) //加入了某个家族
{
auto iter = m_familyList.find(tmpfamilyid);
if (iter != m_familyList.end())
{
//该家族id在map中已经存在
iter->second.push_back(player); //直接把该玩家加入到该家族
}
else
{
//该家族id在map中不存在
list<Fighter*> tmpplayerlist;
m_familyList.insert(make_pair(tmpfamilyid, tmpplayerlist)); //以该家族id为key,增加条目到map中
m_familyList[tmpfamilyid].push_back(player); //向该家族中增加第一个玩家
}
}
}
//将玩家从家族列表中删除
virtual void removeFromList(Fighter* player)
{
int tmpfamilyid = player->GetFamilyID();
if (tmpfamilyid != -1) //加入了某个家族
{
auto iter = m_familyList.find(tmpfamilyid);
if (iter != m_familyList.end())
{
m_familyList[tmpfamilyid].remove(player);
}
}
}
//家族中某玩家说了句话,调用该函数来通知家族中所有人
virtual void notify(Fighter* talker, string tmpContent) //talker是讲话的玩家
{
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中的key表示家族id,value代表该家族中所有玩家列表
map<int, list<Fighter*> > m_familyList;
};
(4)优化后测试案例
// 创建玩家(逻辑同_nmsp1,省略)
// 创建具体通知器
_nmsp2::Notifier* ptalknotify = new _nmsp2::TalkNotifier();
// 玩家加入家族列表(注册为观察者)
ptalknotify->addToList(pplayerobj1);
ptalknotify->addToList(pplayerobj2);
ptalknotify->addToList(pplayerobj3);
ptalknotify->addToList(pplayerobj4);
// 发送聊天(调用通知器广播)
pplayerobj1->SayWords("全族人立即到沼泽地集结,准备进攻!", ptalknotify);
// 王五屏蔽家族信息(移除观察者)
ptalknotify->removeFromList(pplayerobj3);
pplayerobj2->SayWords("请大家听从族长调遣,前往沼泽地!", ptalknotify);
- 效果:第一次聊天张三 / 李四 / 王五均可接收,第二次王五因被移除无法接收,功能满足且效率大幅提升。
完整代码:
// MyProject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
//公众号:程序员速成 ,内含一辈子都让你感激自己的优质视频教程,欢迎关注
#include <iostream>
#include <list>
#include <map>
#ifdef _DEBUG //只在Debug(调试)模式下
#ifndef DEBUG_NEW
#define DEBUG_NEW new(_NORMAL_BLOCK,__FILE__,__LINE__) //重新定义new运算符
#define new DEBUG_NEW
#endif
#endif
//#include <boost/type_index.hpp>
using namespace std;
//#pragma warning(disable : 4996)
namespace _nmsp2
{
class Fighter; //类前向声明
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; //-1表示没有加入任何家族
}
virtual ~Fighter() {} //析构函数
public:
void SetFamilyID(int tmpID) //加入家族的时候要设置家族ID
{
m_iFamilyID = tmpID;
}
int GetFamilyID() //获取家族ID
{
return m_iFamilyID;
}
public:
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; //玩家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) {} //构造函数
};
//聊天信息通知器
class TalkNotifier :public Notifier
{
public:
//将玩家增加到家族列表中来
virtual void addToList(Fighter* player)
{
int tmpfamilyid = player->GetFamilyID();
if (tmpfamilyid != -1) //加入了某个家族
{
auto iter = m_familyList.find(tmpfamilyid);
if (iter != m_familyList.end())
{
//该家族id在map中已经存在
iter->second.push_back(player); //直接把该玩家加入到该家族
}
else
{
//该家族id在map中不存在
list<Fighter*> tmpplayerlist;
m_familyList.insert(make_pair(tmpfamilyid, tmpplayerlist)); //以该家族id为key,增加条目到map中
m_familyList[tmpfamilyid].push_back(player); //向该家族中增加第一个玩家
}
}
}
//将玩家从家族列表中删除
virtual void removeFromList(Fighter* player)
{
int tmpfamilyid = player->GetFamilyID();
if (tmpfamilyid != -1) //加入了某个家族
{
auto iter = m_familyList.find(tmpfamilyid);
if (iter != m_familyList.end())
{
m_familyList[tmpfamilyid].remove(player);
}
}
}
//家族中某玩家说了句话,调用该函数来通知家族中所有人
virtual void notify(Fighter* talker, string tmpContent) //talker是讲话的玩家
{
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中的key表示家族id,value代表该家族中所有玩家列表
map<int, list<Fighter*> > m_familyList;
};
}
int main()
{
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);//程序退出时检测内存泄漏并显示到“输出”窗口
//第5章 观察者(Observer)模式
//(1)一个遍历问题导致的低效率范例
//(2)引入观察者(Observer)模式
//观察者设计模式 定义(实现意图):定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于
//它的对象都会自动得到通知。
//发布-订阅(Publish-Subscribe);
//观察者模式的四种角色
//a)Subject(主题):观察目标,这里指Notifier类。
//b)ConcreteSubject(具体主题):这里指TalkNotifier类。
//c)Observer(观察者):这里指Fighter类。
//d)ConcreteObserver(具体观察者):这里指F_Warrior和F_Mage子类。
//观察者模式的特点:
//a)在观察者和观察目标之间建立了一个抽象的耦合
//b)观察目标会向观察者列表中的所有观察者发送通知。
//c)可以通过增加代码来增加新的观察者或者观察目标,符合开闭原则
//(3)应用联想
//a)救援家族成员镖车
//b)将新闻推荐给符合其胃口的读者
//c)通过改变自身绘制的图形来真实的反应公司的销售数据。
//d)炮楼只会对30米内的玩家(列表内玩家)进行攻击。
//创建游戏玩家
_nmsp2::Fighter* pplayerobj1 = new _nmsp2::F_Warrior(10, "张三"); //实际游戏中很多数据取自数据库。
pplayerobj1->SetFamilyID(100); //假设该玩家所在的家族的家族ID是100
_nmsp2::Fighter* pplayerobj2 = new _nmsp2::F_Warrior(20, "李四");
pplayerobj2->SetFamilyID(100);
_nmsp2::Fighter* pplayerobj3 = new _nmsp2::F_Mage(30, "王五");
pplayerobj3->SetFamilyID(100);
_nmsp2::Fighter* pplayerobj4 = new _nmsp2::F_Mage(50, "赵六");
pplayerobj4->SetFamilyID(200); //赵六和前面三人属于两个不同的家族
//创建通知器
_nmsp2::Notifier* ptalknotify = new _nmsp2::TalkNotifier();
//玩家增加到家族列表中来,这样才能收到家族聊天信息
ptalknotify->addToList(pplayerobj1);
ptalknotify->addToList(pplayerobj2);
ptalknotify->addToList(pplayerobj3);
ptalknotify->addToList(pplayerobj4);
//某游戏玩家聊天,同族人都应该收到该信息
pplayerobj1->SayWords("全族人立即到沼泽地集结,准备进攻!", ptalknotify);
cout << "王五不想再收到家族其他成员的聊天信息了---" << endl;
ptalknotify->removeFromList(pplayerobj3); //将王五从家族列表中删除
pplayerobj2->SayWords("请大家听从族长调遣,前往沼泽地!", ptalknotify);
//释放资源
delete pplayerobj1;
delete pplayerobj2;
delete pplayerobj3;
delete pplayerobj4;
delete ptalknotify;
return 0;
}
1.2.3 观察者模式的核心概念(结合 PPT UML 图)
(1)模式定义
也叫发布 - 订阅(Publish-Subscribe)模式,定义对象间的一对多依赖关系,当观察目标状态改变时,所有依赖的观察者会自动收到通知。
(2)四种核心角色(对应代码)
| 角色 | 对应代码类 | 核心职责 |
|---|---|---|
| Subject(主题 / 观察目标) | Notifier 抽象类 | 定义观察者的添加、移除、通知接口(addToList/removeFromList/notify) |
| ConcreteSubject(具体主题) | TalkNotifier 类 | 维护观察者列表(家族 - 玩家 map),状态变更时调用notify广播通知 |
| Observer(观察者) | Fighter 类 | 定义接收通知的接口(NotifyWords虚函数),被动接收状态变更通知 |
| ConcreteObserver(具体观察者) | F_Warrior/F_Mage | 继承 Observer,可重写NotifyWords实现个性化信息接收逻辑 |
(3)UML 图说明
- 图 5.1(标准观察者 UML):包含
Notifier抽象层,Fighter为观察者基类,TalkNotifier和F_Warrior/F_Mage为具体实现,虚线表示Notifier依赖Fighter; - 图 5.2(未抽象通知器):直接用
TalkNotifier作为观察目标,省略Notifier抽象层,适用于通知逻辑单一的场景; - 图 5.3(角色关系图):具体主题(通知器)状态变更时,主动通知所有具体观察者(家族玩家),实现一对多联动。
(4)模式特点
- 抽象耦合,松绑定:观察目标依赖观察者抽象,而非具体类,满足依赖倒置原则,一方变更不影响另一方;
- 主动通知,简化设计:观察目标主动推送状态变更,无需观察者轮询,降低系统设计复杂度;
- 开闭兼容,易扩展:新增观察者(如弓箭手职业)或观察目标(如系统公告通知器),只需新增子类,无需修改原有代码。
第三节 应用联想
结合模式核心思想,可拓展到以下业务场景:
- 家族成员镖车救援(征途游戏案例)
- 观察目标:被攻击的镖车;观察者:本家族成员;
- 镖车被攻击时(状态变更),自动通知家族成员,成员可点击通知快速驰援。
- 门户网站新闻推荐
- 观察目标:不同类型新闻(国际 / 娱乐 / 美食);观察者:订阅对应分类的用户;
- 新新闻发布时(状态变更),自动推送给订阅该分类的用户,实现精准触达。
- 销售数据多图表展示
- 观察目标:公司销售数据;观察者:饼图 / 柱状图 / 折线图组件;
- 数据更新时(状态变更),同步通知所有图表组件,自动刷新图形展示最新数据。
- 网游炮楼自动攻击
- 观察目标:炮楼;观察者:30 米范围内的玩家;
- 玩家进入 / 离开 30 米范围(状态变更),炮楼更新观察者列表,仅对列表内玩家发起攻击。
代码迭代总结
| 迭代阶段 | 存储结构 | 遍历范围 | 耦合程度 | 扩展性 | 效率 |
|---|---|---|---|---|---|
| 第一版(_nmsp1) | 全局 list | 全服玩家 | 高耦合 | 差 | 极低 |
| 第二版(_nmsp2) | 家族分组 map | 仅当前家族玩家 | 低耦合 | 好 | 极高 |
观察者模式通过抽象解耦和分组管理,既解决了遍历效率问题,又满足了开闭原则,是一对多依赖场景的最优解之一。
参开资料来源:《C++新经典-设计模式》

浙公网安备 33010602011771号