在对象间定义一种一对多的依赖关系,以便当某对象的状态改变时,与它存在依赖关系的所有对象都能收到通知并自动进行更新。
(摘自《游戏编程模式》)
我们很熟悉的MVC模式,其底层就是观察者模式。Java中的java.util.Observer和C#的event更是将观察者模式集成于语言层面中。
要如何理解观察者模式,从目的上讲,观察者模式是为了使代码在逻辑层面更加统一。为何这么说呢?我们来看一个例子。在开发成就系统的时候,涉及获得成就条件的判定可以遍布所有的游戏逻辑(例如获得一万金币:涉及金币系统;坠落水中:涉及物理系统;行走100公里:涉及角色系统)。我们当然可以这么写:
//金币系统
class CoinSystem
{
void CoinSystemUpdate()
{
//金币系统相关代码...
if(coinNum>10000)
GetArchievements(ArchType.TenThousandCoins);
}
}
//物理系统
class PhysicsSystem
{
void PhysicsSystemUpdate()
{
//物理系统相关代码...
if(isDropWater)
GetArchievements(ArchType.DropWater);
}
}
//角色系统
class CharacterSystem
{
void PhysicsSystemUpdate()
{
//角色系统相关代码...
if(walkLength>100)
GetArchievements(ArchType.WalkHundredKilometer);
}
}
显而易见的是,我们在不同系统中插入了成就系统获得条件的判定代码。尽管很好理解,但这也导致了代码逻辑不统一,散落在各个模块中,既不优雅,也不便于维护。利用观察者模式可以对这些代码进行改进。
应用观察者模式实现
既然我们想要模块化上面的代码,我们就需要让各个系统的代码不再进行成就达成条件判定,而是将相关判定数据直接传递给成就系统(通常就是某个模块的实例)——尽管在系统中还是增添了与该系统无关代码(被观察者发出通知),但是比上面直接处理成就系统要优雅的多。下面我们将对代码进行改进。
观察者
成就系统作为观察者,它需要处理从被观察者发出来的消息,并根据消息类型处理相关逻辑。成就系统代码如下,所有的成就解锁条件将在成就系统接受消息的函数中进行判断:
//观察者基类
class Observer
{
public:
virtual ~Observer();
virtual void OnNotify(const Object& sender,EventType event);
}
//成就系统
class Archievements : public Observer
{
public:
virtual void OnNotify(const Object& sender,EventType event)
{
switch((ArchType)event)
{
case ArchType.TenThousandCoins:
if(sender.coinNum>10000)
Unlock(event);
break;
case ArchType.DropWater:
if(sender.isDropWater)
Unlock(event);
break;
case ArchType.WalkHundredKilometer:
if(sender.walkLength>100)
Unlock(event);
break;
//其他事件...
}
}
private:
void Unlock(ArchType type);
}
被观察者如何实现?
被观察者需要怎么实现。被观察者就是需要将消息通知给观察者的,由于观察对象不只1个,因此,被观察者需要维护一个观察者列表。且不断地将消息发送给被观察者。实现代码如下图所示:
//被观察者基类
class Subject
{
private:
vector<Observer*> observerList;
public:
void SendNotify(const Object& sender,EventType event)
{
for(int index=0;index<observerList.size();index++)
{
observerList[index].OnNotify(sender,event);
}
}
public:
void AddObserver(Observer* observer)
{
observerList.push_back(observer);
}
void RemoveObserver(Observer* observer)
{
for(int index=0;index<observerList.size();index++)
{
if(observerList[index]==observer)
{
observerList.erase(observerList.begin()+index);
break;
}
}
}
}
//我们可以使用"Have One"模式而非继承来处理 拓展成为被观察者。
class CoinSystem
{
public:
Subject subject;
void CoinSystemUpdate()
{
subject.SendNotify(this,ArchType.GetXXCoins);
}
}
//只要观察者订阅了被观察者,消息就会不断被发送。
coinSystemInstance.subject.AddObserver(ArchievementsInstance);
physicsSystemInstance.subject.AddObserver(ArchievementsInstance);
characterSystemInstance.subject.AddObserver(ArchievementsInstance);
观察者模式使用时需要注意或缺点
- 远离UI线程:由于观察者模式是同步的,因此,任何一个处理通知的时候都有可能导致阻塞。如果在UI线程使用观察者模式,很有可能让用户感到突如其来的卡顿。
- 使用过多动态内存分配。使用固定数组可以避免动态内存分配,但局限在于一个固定观察者数量是有上限的。用stl的容器不失为一个好方法;或者用链表管理亦可;对象池也可以解决此问题。
- 观察者和被观察者的销毁问题。观察者销毁后被观察者管理的列表中仍引用该观察者会引发空报错问题。解决该问题的方法是:当观察者销毁时,需要自动移除列表(可将移除代码编写在观察者类的析构函数中)。也就是说,添加和删除应该是一一对应的。
class Archievements : public Observer
{
public:
Archievements()
{
//...
coinSystemInstance.subject.AddObserver(ArchievementsInstance);
physicsSystemInstance.subject.AddObserver(ArchievementsInstance);
characterSystemInstance.subject.AddObserver(ArchievementsInstance);
}
~Archievements()
{
//...
coinSystemInstance.subject.RemoveObserver(ArchievementsInstance);
physicsSystemInstance.subject.RemoveObserver(ArchievementsInstance);
characterSystemInstance.subject.RemoveObserver(ArchievementsInstance);
}
}
总结
- 观察者模式可以形象的描述为:被观察者为报社,观察者为读报用户;报社管理一个订报用户列表,并向每个订报用户发送报纸;而读报用户可以选择订阅报纸或退订报纸。订报可获得信息;不订或退订则无法获取信息。
- 观察者模式非常适合于一些不相关的模块之间的通信问题。它不适合于单个紧凑的模块内部的通信。