文章目录
观察者模式具体的应用情景
假设你还是游戏公司的程序员。策划想让你对游戏增加“家族”(或者“公会”)的功能,每个玩家都有一个家族id,家族id相同则代表属于同一个家族。
家族频道有聊天功能,只有同一个家族的人才能收到家族内部人员的信息。
你该如何实现这个功能呢?
首先我们想到的肯定是,用一个全局列表,也就是数组或者链表将所有玩家保存起来,当有一个人发送信息时,就去遍历整个全局列表,找到家族id相同的人,发给这个人信息。
说干就干,首先写出第一版代码:
#include <iostream>
#include <list>
using namespace std;
namespace hjl_project1
{
class Fighter;
// list保存所有玩家
list<Fighter *> g_playList;
class Fighter
{
public:
Fighter(int tmpID, string tmpName)
: m_iPlayerID(tmpID), m_sPlayerName(tmpName), m_iFamilyID(-1) //-1表示没有加入任何家族
{
}
virtual ~Fighter() {}
//设置家族id
void SetFamilyID(int familyID)
{
m_iFamilyID = familyID;
}
//这个玩家发了一条信息
void SayWords(string tmpContent)
{
//将这条信息传给同家族的所有玩家
if (m_iFamilyID != -1)
{
for (auto iter : g_playList)
{
if (iter->m_iFamilyID == this->m_iFamilyID)
NotifyWords(iter, tmpContent);
}
}
}
private:
void NotifyWords(Fighter *otherPlayer, string tmpContent)
{
//显示信息
cout << "玩家:" << otherPlayer->m_sPlayerName << "收到了玩家:" << m_sPlayerName << "的信息:" << tmpContent << endl;
}
int m_iPlayerID;
string m_sPlayerName;
//家族id
int m_iFamilyID;
};
//战士类
class F_Warrior : public Fighter
{
public:
F_Warrior(int tmpID, string tmpName) : Fighter(tmpID, tmpName) {}
};
//法师类
class F_Mage : public Fighter
{
public:
F_Mage(int tmpID, string tmpName) : Fighter(tmpID, tmpName) {}
};
}
int main()
{
using namespace hjl_project1;
Fighter *player1 = new F_Warrior(10, "张三");
player1->SetFamilyID(100); //假设该玩家的家族id是100
g_playList.push_back(player1); //加入到全局列表中
Fighter *player2 = new F_Warrior(11, "李四");
player2->SetFamilyID(100); //假设该玩家的家族id是100
g_playList.push_back(player2);
Fighter *player3 = new F_Warrior(12, "王五");
player3->SetFamilyID(101); //假设该玩家的家族id是101
g_playList.push_back(player3);
Fighter *player4 = new F_Warrior(13, "赵六");
player4->SetFamilyID(100); //假设该玩家的家族id是100
g_playList.push_back(player4);
//当聊天室,某个玩家发送消息,同一个家族的人都会收到信息
player1->SayWords("全家族的人立刻集合!");
}
上面的代码虽然实现了家族聊天的功能,但是效率并不高,因为每一次都要遍历全局列表,找到相同家族id的玩家,如果玩家数量很多,运行效率就会大打折扣。
下面引入观察者模式。
如果把所有的家族id相同的玩家收集到一个列表中,那发送信息时,就只需要遍历该玩家所在家族的列表,并向这个列表中所有的玩家发送消息即可。因为我们的“家族”是有上限的,比如一个“家族”只有100人,那么最多只需要遍历100个玩家即可。
我们使用一个通知器类来管理这个列表中的玩家。
namespace hjl_project2
{
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;
};
class Fighter
{
public:
Fighter(int tmpID, string tmpName)
: m_iPlayerID(tmpID), m_sPlayerName(tmpName), m_iFamilyID(-1) //-1表示没有加入任何家族
{
}
virtual ~Fighter() {}
//设置家族id
void SetFamilyID(int familyID)
{
m_iFamilyID = familyID;
}
//这个玩家发了一条信息
void SayWords(string tmpContent, Notifier *notifier)
{
notifier->notify(this, tmpContent);
}
int GetFamilyID()
{
return m_iFamilyID;
}
//通知该玩家接收到其他玩家发送来的信息
virtual void NotifyWords(Fighter *talkPlayer, string tmpContent)
{
//显示信息
cout << "玩家:" << m_sPlayerName << "收到了玩家:" << talkPlayer->m_sPlayerName << "的信息:" << tmpContent << endl;
}
private:
int m_iPlayerID;
string m_sPlayerName;
//家族id
int m_iFamilyID;
};
//战士类
class F_Warrior : public Fighter
{
public:
F_Warrior(int tmpID, string tmpName) : Fighter(tmpID, tmpName) {}
};
//法师类
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 itermap = m_familylist.find(tmpfamilyid);
if (itermap != m_familylist.end())
{
itermap->second.push_back(player);
}
else
{
list<Fighter *> tmpplayerlist;
tmpplayerlist.push_back(player);
m_familylist.insert({tmpfamilyid, tmpplayerlist});
}
}
}
//将玩家从家族列表中删除
virtual void removeFromList(Fighter *player)
{
int tmpfamilyid = player->GetFamilyID();
if (tmpfamilyid != -1)
{
auto itermap = m_familylist.find(tmpfamilyid);
if (itermap != m_familylist.end())
{
m_familylist[tmpfamilyid].remove(player);
}
}
}
//家族中某个玩家说了一句话,调用通知函数来通知家族中所有的人
virtual void notify(Fighter *talker, string tmpContent)
{
int tmpfamilyid = talker->GetFamilyID();
if (tmpfamilyid != -1)
{
auto itermap = m_familylist.find(tmpfamilyid);
if (itermap != m_familylist.end())
{
//遍历该玩家所属家族的所有成员
for (auto iter : itermap->second)
{
iter->NotifyWords(talker, tmpContent);
}
}
}
}
private:
//家族id,该家族id中所有玩家的列表
map<int, list<Fighter *>> m_familylist;
};
}
int main()
{
using namespace hjl_project2;
//创建通知器
Notifier *ptalknotify = new TalkNotifier();
Fighter *player1 = new F_Warrior(10, "张三");
player1->SetFamilyID(100); //假设该玩家的家族id是100
//将玩家增加到列表中来
ptalknotify->addToList(player1);
Fighter *player2 = new F_Warrior(11, "李四");
player2->SetFamilyID(100); //假设该玩家的家族id是100
ptalknotify->addToList(player2);
Fighter *player3 = new F_Warrior(12, "王五");
player3->SetFamilyID(101); //假设该玩家的家族id是101
ptalknotify->addToList(player3);
Fighter *player4 = new F_Warrior(13, "赵六");
player4->SetFamilyID(100); //假设该玩家的家族id是100
ptalknotify->addToList(player4);
//某个玩家发送信息
player1->SayWords("全家族的人立刻集合!", ptalknotify);
//赵六退出了家族
ptalknotify->removeFromList(player4);
player1->SayWords("发起进攻!", ptalknotify);
}
观察者模式的定义
定义对象之间的一对多(“被观察者”和“观察者”)的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会自动得到通知。也就是说,“被观察者”可以控制“观察者”的行为,类似于交通信号灯控制行人的行为。
比如上面的例子中,所有玩家都依赖于一个通知器,玩家每发送一条消息,所有依赖于这个通知器的玩家都会收到内容,通知器可以将这条消息显示地发送给同一个家族的玩家。
观察者模式也叫做发布-订阅模式。
观察者模式中一般包含四种角色:
- Subject主题(观察目标),在上面的例子中指Notifier类,提供观察目标的接口(添加,删除,通知)。
- ConcreteSubject具体主题,维护一个观察列表,这里指TalkNotifier类。
- Observer观察者,这里指Fighter,当被观察者的状态发生变化时,观察者会收到通知。
- ConcreteOberver具体观察者,调用主题的接口将自己添加到观察列表中。
观察者模式的特点:
- 在观察者和观察目标之间建立了一个抽象的松耦合。观察目标只需要维持一个抽象的观察者列表,并不需要了解具体的观察者类。
- 观察目标会向观察列表中的所有观察者发送通知,而不是让观察者不断查询观察目标的状态,从而简化了一对多系统的设计难度。
- 可以增加代码的方式,增加观察者和观察目标,符合“开闭原则”。
观察者的应用联想:
- 还是以上面的例子,如果家族成员中某个人受到了攻击,就可以通知家族中的所有的人,请求他们前去支援。
- 实现推送的功能。比如一个博主发了一条视频,就可以用观察者模式将这篇新闻推送给关注这个博主的人。
- 某公司的产品数据由折线图、树状图、饼图等统计显示,如果产品数据发生了变化,就要通知这几种图形的观察者,从而修改这些图形的数据。
- 比如一个哨兵机器人会对附近30米之内的玩家进行攻击,哨兵机器人会维持一份列表来记录30米之内的玩家,因为玩家是不断移动的,所以这个列表中会不断被添加和删除玩家信息。