本书中主要介绍设计模式,经常以游戏中的某些,情况来解释设计模式!

随便打开电脑中的一个应用,很有可能它就使用了MVC架构, 而究其根本,是因为观察者模式。 观察者模式应用广泛,Java甚至将其放到了核心库之中(java.util.Observer),而C#直接将其嵌入了语法event关键字)。

随便打开电脑中的一个应用,很有可能它就使用了MVC架构, 而究其根本,是因为观察者模式。 观察者模式应用广泛,Java甚至将其放到了核心库之中(java.util.Observer),而C#直接将其嵌入了语法event关键字)。

成就解锁

假设我们向游戏中添加了成就系统。 它存储了玩家可以完成的各种各样的成就,比如“杀死1000只猴子恶魔”,“从桥上掉下去”,或者“一命通关”。

要实现这样一个包含各种行为来解锁成就的系统是很有技巧的。 如果我们不够小心,成就系统会缠绕在代码库的每个黑暗角落。 当然,“从桥上掉落”和物理引擎相关, 但我们并不想看到在处理撞击代码的线性代数时, 有个对unlockFallOffBridge()的调用是不?

我们喜欢的是,照旧,让关注游戏一部分的所有代码集成到一块。 挑战在于,成就在游戏的不同层面被触发。怎么解耦成就系统和其他部分呢?

这就是观察者模式出现的原因。 这让代码宣称有趣的事情发生了,而不必关心到底是谁接受了通知。

举个例子,有物理代码处理重力,追踪哪些物体待在地表,哪些坠入深渊。 为了实现“桥上掉落”的徽章,我们可以直接把成就代码放在那里,但那就会一团糟。 相反,可以这样做:

  1. void Physics::updateEntity(Entity& entity)
  2. {
  3. bool wasOnSurface = entity.isOnSurface();
  4. entity.accelerate(GRAVITY);
  5. entity.update();
  6. if (wasOnSurface && !entity.isOnSurface())
  7. {
  8. notify(entity, EVENT_START_FALL);
  9. }
  10. }

它做的就是声称,“额,我不知道有谁感兴趣,但是这个东西刚刚掉下去了。做你想做的事吧。”

成就系统注册它自己为观察者,这样无论何时物理代码发送通知,成就系统都能收到。 它可以检查掉落的物体是不是我们的失足英雄, 他之前有没有做过这种不愉快的与桥的经典力学遭遇。 如果满足条件,就伴着礼花和炫光解锁合适的成就,而这些都无需牵扯到物理代码。

事实上,我们可以改变成就的集合或者删除整个成就系统,而不必修改物理引擎。 它仍然会发送它的通知,哪怕实际没有东西接收。

它如何运作

如果你还不知道如何实现这个模式,你可能可以从之前的描述中猜到,但是为了减轻你的负担,我还是过一遍代码吧。

观察者

我们从那个需要知道别的对象做了什么事的类开始。 这些好打听的对象用如下接口定义:

  1. class Observer
  2. {
  3. public:
  4. virtual ~Observer() {}
  5. virtual void onNotify(const Entity& entity, Event event) = 0;
  6. };

任何实现了这个的具体类就成为了观察者。 在我们的例子中,是成就系统,所以我们可以像这样实现:

  1. class Achievements : public Observer
  2. {
  3. public:
  4. virtual void onNotify(const Entity& entity, Event event)
  5. {
  6. switch (event)
  7. {
  8. case EVENT_ENTITY_FELL:
  9. if (entity.isHero() && heroIsOnBridge_)
  10. {
  11. unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
  12. }
  13. break;
  14. // 处理其他事件,更新heroIsOnBridge_变量……
  15. }
  16. }
  17. private:
  18. void unlock(Achievement achievement)
  19. {
  20. // 如果还没有解锁,那就解锁成就……
  21. }
  22. bool heroIsOnBridge_;
  23. };

被观察者

被观察的对象拥有通知的方法函数,用GoF的说法,那些对象被称为“主题”。 它有两个任务。首先,它有一个列表,保存默默等它通知的观察者:

  1. class Subject
  2. {
  3. private:
  4. Observer* observers_[MAX_OBSERVERS];
  5. int numObservers_;
  6. };

重点是被观察者暴露了_公开的_API来修改这个列表:

  1. class Subject
  2. {
  3. public:
  4. void addObserver(Observer* observer)
  5. {
  6. // 添加到数组中……
  7. }
  8. void removeObserver(Observer* observer)
  9. {
  10. // 从数组中移除……
  11. }
  12. // 其他代码……
  13. };

这就允许了外界代码控制谁接收通知。 被观察者与观察者交流,但是不与它们耦合。 在我们的例子中,没有一行物理代码会提及成就。 但它仍然可以与成就系统交流。这就是这个模式的聪慧之处。

被观察者有一列表观察者而不是单个观察者也是很重要的。 这保证了观察者不会相互干扰。 举个例子,假设音频引擎也需要观察坠落事件来播放合适的音乐。 如果客体只支持单个观察者,当音频引擎注册时,就会取消成就系统的注册。

这意味着这两个系统需要相互交互——而且是用一种极其糟糕的方式, 第二个注册时会使第一个的注册失效。 支持一列表的观察者保证了每个观察者都是被独立处理的。 就它们各自的视角来看,自己是这世界上唯一看着被观察者的。

被观察者的剩余任务就是发送通知:

  1. class Subject
  2. {
  3. protected:
  4. void notify(const Entity& entity, Event event)
  5. {
  6. for (int i = 0; i < numObservers_; i++)
  7. {
  8. observers_[i]->onNotify(entity, event);
  9. }
  10. }
  11. // 其他代码…………
  12. };

可被观察的物理系统

现在,我们只需要给物理引擎和这些挂钩,这样它可以发送消息, 成就系统可以和引擎连线来接受消息。 我们按照传统的设计模式方法实现,继承Subject:

  1. class Physics : public Subject
  2. {
  3. public:
  4. void updateEntity(Entity& entity);
  5. };

这让我们将notify()实现为了Subject内的保护方法。 这样派生的物理引擎类可以调用并发送通知,但是外部的代码不行。 同时,addObserver()和removeObserver()是公开的, 所以任何可以接触物理引擎的东西都可以观察它。

现在,当物理引擎做了些值得关注的事情,它调用notify(),就像之前的例子。 它遍历了观察者列表,通知所有观察者。

image.png
很简单,对吧?只要一个类管理一列表指向接口实例的指针。 难以置信的是,如此直观的东西是无数程序和应用框架交流的主心骨。

观察者模式不是完美无缺的。当我问其他程序员怎么看,他们提出了一些抱怨。 让我们看看可以做些什么来处理这些抱怨。