重返设计模式--观察者模式
理论要点
什么是观察者模式:观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。通俗点讲就是一方发送某条消息,其它只要注册了这条消息的对象都会收到通知。其中我们把发送方称为被观察者,接受方称为观察者。
要点:
1,我们知道,将一个系统分割成一个一些类相互协作的类有一个不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便。观察者就是解决这类的耦合关系的。
2,目前广泛使用的MVC模式,究其根本,是基于观察者模式的。把数据与表现分开,通过发消息与接受消息交互数据。
3,观察者模式应用广泛,Java甚至将其放到了核心库之中(java.util.Observer),而C#直接将其嵌入了语法(event关键字)中。使用场合:
1,游戏里数据模块与UI表现模块分开,通过观察者模式通信。
2,游戏里成就系统,小红点等。
3,服务端与客服端之间数据的通信。
代码分析
1,我们就已游戏的成就系统切入,假设当某个成就满足条件时,便会触发成就系统并放射礼花。如:“触发主角从桥上坠落”成就。好,坠落条件不用想是和物理引擎相关,所以我们的发送消息应该是在物理引擎中判断,如:
void Physics::updateEntity(Entity& entity)
{bool wasOnSurface = entity.isOnSurface(); //实体是否在地面之上entity.accelerate(GRAVITY);entity.update();if(wasOnSurface && !entity.isOnSurface()){//发送开始坠落消息notify(entity, EVENT_START_FALL);}
}
恩,这还相当于是伪代码,到底是怎么发送与接受的呢?下面我们来具体分析下实现观察者模式的原理。
我们首先来看接受消息方(观察者):
class Observer
{
public:virtual ~Observer() {}//接受通知接口virtual void onNotify(const Entity& entity, Event event) = 0;
};
那么就是这样,物理引擎中检查到了条件满足(坠落条件)发送消息,然后触发成就系统解锁对应成就。因此,成就系统对象应该继承观察者类并实现接受通知接口:
class Achievements : public Observer
{
public:virtual void onNotify(const Entity& entity, Event event){switch (event){case EVENT_START_FALL:if (entity.isHero()){unlock(ACHIEVEMENT_FALL_OFF_BRIDGE);}break;// 处理其他事件……}}private:void unlock(Achievement achievement){// 如果还没有解锁,那就解锁成就……}
};
然后,看看发送消息方(被观察者):
class Subject
{
public:void addObserver(Observer* observer){// 添加到数组中……}void removeObserver(Observer* observer){// 从数组中移除……}protected://发送消息void notify(const Entity& entity, Event event){for (int i = 0; i < _numObservers; i++){_observers[i]->onNotify(entity, event);}}private:Observer* _observers[MAX_OBSERVERS];int _numObservers;
};
好,观察者与被观察者我们都实现了,现在只要让我们的物理引擎对象继承被观察者,就可以在物理引擎对象中直接发送消息了:
class Physics : public Subject
{
public:void updateEntity(Entity& entity);
};
这样再回到最前面的“触发主角从桥上坠落”的事件发送处,是不是原理很简单,豁然开朗!它解耦了物理系统和成就系统,让这本来就不相干的两个系统完全分开。好,其实这就是我们的观察者模式,原理到此就讲完了~
2,代码只要你想优化往往它是总能优化的,先想想上面的实现方式有没有不合理的地方?好,开始进入一步步优化的过程,首先,在上面被观察者类中,我使用的是定长数组,因为我想尽可能保证简单。 但在真实的项目中,观察者列表随着观察者的添加和删除而动态地增长和缩短。 这种动态内存的频繁分配是不好管理也是损耗效率的。那么既然知道问题所在了我们应该怎么优化了?内存分配耗效率是吧,那么我们想想能不能让被观察者不管理观察者对象的内存,观察者对象创建应该它自己管理。还有要知道,我们通常涉及到容器实现无非两种结构:数组和链表。好,我们看看下面的改写方式即把原来的数组结构改成链表结构,被观察者(Subject)内的观察者(Observer)链表表示:
这样的话,被观察者中就只有一个指向观察者的头指针了,观察者的对象自己管理。
先看看改写后的观察者类:
class Observer
{//把被观察者声明为其友元类,这样就可以直接在观察者类里面管理自己的事件添加与删除了friend class Subject;public:Observer() : _next(NULL) {}// 其他代码……
private:Observer* _next;
};
然后是改写后的被观察者类:
class Subject
{
public:Subject() : _head(NULL) {}void addObserver(Observer* observer){//头插法(这种方式简单,但事件响应顺序是反的,如果事件响应对顺序有要求就用尾插法吧~)observer->_next = _head;_head = observer;}void removeObserver(Observer* observer){if (_head == observer){_head = observer->_next;observer->_next = NULL;return;}Observer* current = _head;while (current != NULL){if (current->_next == observer){current->_next = observer->_next;observer->_next = NULL;return;}current = current->_next;}}void notify(const Entity& entity, Event event){Observer* observer = _head;while (observer != NULL){observer->onNotify(entity, event);observer = observer->_next;}}private:Observer* _head;
};
这种实现还不错,对吧?被观察者现在想有多少观察者就有多少观察者,而且添加和删除观察者并不会造成任何动态内存分配。 注册和移除观察者的操作和使用普通数组一样快。
注:链表有两种风格。学校教授的那种,节点对象包含数据。 而我们的例子中,是另一种: 数据(这个例子中是观察者)包含了节点(_next指针)。后者的风格被称为“侵入式”链表,因为在对象内部使用链表侵入了对象本身的定义。 侵入式链表灵活性更小,但如我们所见,也更有效率。 在Linux核心这样的地方这种风格很流行。
不过注意观察,Subject链表中是直接用Observer作为链表节点,这暗示着一个观察者一次只能存在于一个被观察者中。 然而,在传统的实现中,每个被观察者有独立的列表,一个观察者同时可以存在于多个列表中。
你也许可以接受这一限制。 因为通常是一个被观察者有多个观察者,反过来就很少见了。 如果这真是一个问题,这里还有一种不必使用动态分配的解决方案。 详细介绍的话,这章就太长了,我就只提供个思路,其它自行脑补:
其实上面注释中已经提到了两种风格链表:节点包含数据和数据包含节点,我们只需把风格改成前者就可以了,节点不再是观察者本身, 它包含了指向观察者的指针和指向链表下一节点的指针。
这样多个节点就可以指向同一观察者,这就意味着一个观察者可以同时存在于多个被观察者的列表中。 我们可以同时观察多个对象了(多对多)。
避免动态分配的方法很简单:由于这些节点都是同样大小和类型, 可以预先在对象池中分配它们。 这样你只需处理固定大小的列表节点,可以随你所需使用和重用, 而无需牵扯到真正的内存分配器。
嘛蛋,这里文字越来越多了,人们都是喜欢看代码而不愿看这迷糊的文字的,我也是。然而,没办法了,谁叫它注意点有这么多了~
最后一个注意点:野指针
当删除一个被观察者或观察者时会发生什么? 如果你不小心的在某些观察者上面调用了delete,被观察者也许仍然持有指向它的指针。 那是一个指向一片已释放区域的野指针。 当被观察者试图发送一个通知,额……这时发生的事情会出乎你的意料之外吧。
你可以用好几种方式处理这点。 最简单的就是,在被删除时取消注册是观察者的职责。 多数情况下,观察者确实知道它在观察哪个被观察者, 所以通常需要做的只是在它的析构函数中添加一个removeObserver()就行了(友元类)。
然而通常情况下,难点不在如何做,而在记得做。人,哪怕是那些花费在大量时间在机器前,拥有让我们黯然失色才能的人——也是绝对地不可靠。 这就是为什么我们发明了电脑:它们不像我们那样经常犯错误。
更安全的方案是在每个被观察者销毁时,让观察者自动取消注册。 如果你在观察者基类中实现了这个逻辑,每个人不必记住就可以使用它。 这确实增加了一定的复杂度。 这意味着每个观察者都需要有它在观察的被观察者的列表。 最终维护一个双向指针。
好,不多讲了,实际观察者模式要么语法都直接支持要么就是引擎自带提供了,而且它们肯定还做了各种优化,我们这里只是讲解思路,最后用一句话概括原理就是:先把消息添加进容器,发送消息时遍历这个容器,触发对应监听。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
