游戏编程设计模式 -- 观察者模式

Game Programming Patterns -- Observer

原文地址:http://gameprogrammingpatterns.com/observer.html
原作者:Robert Nystrom

原创翻译,转载请注明出处

如果你朝一台电脑丢一块石头的话,你肯定总是能砸中一个使用Model-View-Controller架构搭建的应用,而MVC的底层使用的就是观察者模式。观察者模式是非常普及的,Java把它放在了核心库(java.util.Observer)中,而C#把它嵌入到了语言中(event关键字) 。

和软件开发中很多东西一样,MVC在70年代的时候被使用Smalltalk的程序员们发明出来。而使用Lisp语言的开发者可能会声称MVC是它们在60年代发明的。

观察者模式是GOF提出的模式中最为广泛使用和广为人知的模式之一,但是我们的游戏开发世界有的是显得有点与世隔绝,所以这对你来说可能是一个全新的知识。那么接下来让我来带你看一个激发你灵感的例子吧。

解锁成就

比方说我们要为我们的游戏添加一个成就系统。它包括了一系列不同的奖章,玩家可以通过在游戏中完成特定的里程碑来获取,比如“杀死100个猴子恶魔”、“从桥上掉落”或者“只装备一只亡灵黄鼠狼完成关卡”等。

游戏编程设计模式 -- 观察者模式_第1张图片

我发誓我在画这张图的时候脑子里完全没有什么别的想法。

想要干净利落地实现这个功能其实是有一点棘手的,因为我们的游戏里有非常多的通过各种不同类型行为来解锁的成就。如果我们不够小心的话,成就系统的藤蔓将蔓延到我们代码库的每一个黑暗的角落。当然,“从桥上掉落”是通过某种方式和物理引擎相关联的,但是我们真的想要看到在我们的碰撞检测算法中去调用一个tounlockFallOffBridge()方法?

这是一个反问。没有任何一个有点自尊心的物理引擎程序员会让我们用诸如游戏玩法之类的代码去玷污他们完美的算法。

我们总是想要把游戏中关于一块功能的代码放到一个地方。这里有点挑战性的是,成就事通过游戏过程中很多不同的方面来触发的。我们要如何来实现成就,才能不把它的代码耦合到所有触发成就的功能代码中呢?

这就是观察者模式被设计出来的目的。它让一段代码去通知有些有趣的事情发生了,而并不管有哪些代码收到了这个通知。

举个例子,我们写了一段物理代码来控制对象在一个平面上或者坠落中的重力作用和运动轨迹。为了实现“从桥上掉落”这个成就,我们可以直接把成就代码塞到物理代码里,但是那样的话就太糟糕了。取而代之的是,我们可以这样做:

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;
};

onNotify()方法的参数你可以自己决定。因为这是观察者模式,而不是可以直接粘贴到你游戏里的观察者代码。典型的参数是发送通知的对象和一个用来保存一些数值的data参数。
如果你使用的是支持泛型或者模板的编程语言,你在这里可以使用使用它们来表示发送通知的对象,不过把它们整理成适合你自己的用例也是一个不错的选择。这里为了方便,我只硬编码了一个entity和一个enum来描述发生的事情。

任何一个具体的类都可以通过实现这个接口成为一个观察者。在我们的例子里,就是成就系统,所以我们接下来这么做:

class Achievements : public Observer
{
public:
  virtual void onNotify(const Entity& entity, Event event)
  {
    switch (event)
      {
      case EVENT_ENTITY_FELL:
        if (entity.isHero() && heroIsOnBridge_)
        {
          unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
        }
        break;

        // Handle other events, and update heroIsOnBridge_...
      }
  }
private:
  void unlock(Achievement achievement)
  {
    // Unlock if not already unlocked...
  }
  
  bool heroIsOnBridge_;
};
被观察对象

通知方法是在那些被监听的对象中调用的。在GOF的描述中,这些对象被称为“被观察对象(subject)”。被观察对象有两个功能要实现。首先,它保存了一个等待接收它发出通知的所有观察者的列表:

class Subject
{
  private:
  Observer* observers_[MAX_OBSERVERS];
  int numObservers_;
};

在实际的代码中,你可以使用动态的集合来代替固定长度的数组。在这里我使用了比较基础的数组,以方便使用其他编程语言、不知道C++标准库的读者。

很重要的一点是,被观察者暴露了一个public接口用于修改这个列表:

class Subject
{
public:
  void addObserver(Observer* observer)
  {
    // Add to array...
  }

  void removeObserver(Observer* observer)
  {
    // Remove from array...
  }

  // Other stuff...
};

这可以让外部代码来控制哪些代码可以收到通知。被观察者可以和观察者们进行通讯,但是和它们并没有耦合。在我们的例子中,没有任何一行物理引擎代码会提到成就这个东西。然而,它依旧可以和成就系统进行交流。这就是观察者模式最机智的部分。

而同样重要的是,被观察者保存了一个观察者的列表,而不是仅仅一个观察者。这就保证了观察者之间没有隐藏的耦合关系。举个例子,假如声音引擎也关注了我们的掉落事件,这样它可以播放一个合适的音效。如果被观察者只支持一个观察者的话,当声音引擎需要注册监听时,它必须把成就系统的监听注销。

这意味着这两个系统会产生相互干扰,并且是一种非常讨厌的干扰,因为后一个注册的会禁用前一个的监听。支持一个观察者列表可以保证观察者之间是相互独立的。观察者所能知道的是,这个世界上只有它自己是在关注被观察者的。

被观察者的另外一个工作就是发送通知:

class Subject
{
protected:
  void notify(const Entity& entity, Event event)
  {
    for (int i = 0; i < numObservers_; i++)
    {
      observers_[i]->onNotify(entity, event);
    }
  }

  // Other stuff...
};

注意这里的代码假设了观察者们没有修改它们onNotify()方法的参数列表。一个更健壮的实现需要可以避免或者控制这种并发修改。

可以观察的物理引擎

现在,我们只需要把这些和物理引擎连接起来让它可以发送通知,而成就系统中可以写一段代码来接收这个通知。我们遵循最基本的设计模式方法,继承Subject类:

class Physics : public Subject
{
public:
  void updateEntity(Entity& entity);
}

在实际开发中,在这里我会避免使用继承,而是在Physics类中声明一个Subject类的实例。我们不再观察整个物理引擎,而是去观察“掉落事件”(falling event)这个对象。观察者们可以像下面这样来注册监听:

对于我来说,这两个的区别就是“观察者”系统和“事件”系统。前者是在观察会做一些有趣的事情的对象,而后者则是在观察这件有趣的事情本身。

这让我们可以把notify()写成Subject的protected方法。如此Physics类通过继承来调用notify()去发送通知,但是外部的代码则没有这个权限。同时,addObserver()和removeObserver()都是public的,所以任何和物理引擎相关的代码都可以对它进行观察。

现在,当物理引擎做了一些值得外界关注的事情之后,它会像我们一开始的那个例子中一样调用notify()方法。这个方法将遍历整个观察者列表,通知它们注意有事情发生了。

游戏编程设计模式 -- 观察者模式_第2张图片

很简单,不是么?只需要一个类去维护某个接口的实例列表。让人不敢相信的是,如此简单的方法正是无数的程序和app框架通讯系统的支柱。

不过观察者模式也并不是没有反对者的。当我跟其他游戏开发者问及他们是如何看待观察者模式的时候,他们提出了一些控诉。下面来看看我们是如何来解决这些问题的。

“它太慢了”

这一点是我听到的被抱怨最多的,通常可以从那些并不真正了解观察者模式的程序员口中听到。他们先入为主地认为任何和“设计模式”有关的东西肯定包含了一堆的类、间接性以及一些想着法子浪费CPU资源的点子。

观察者模式得到了格外糟糕的批评,因为它通常和“event”、“messages”以及“data binding”这些关键字关联在一起。这些系统中的一些可能会很慢(设计如此)。它们包含了队列或者给每条通知动态分配内存这些东西。

这就是为什么我认为把设计模式文档化是一件很重要的事情。当我们队专业术语理解不够清楚的时候,我们就失去了清晰和简洁地进行交流的能力。当你说“Observer”的时候,其他人可能会认为你在说“Events”或者“Messaging”,因为从来没有人愿意费心去写清楚这些之间的区别,而其他人自然也就没有机会去了解。
这正是我尝试通过这本书去做的事情。在接下来的章节里,会有一章关于events和messages的:事件队列(Event Queue)

但是,既然你在之前的例子中了解到了这个模式是如何实现的,你就会知道速度并没有收到太大的影响。发送通知时只是简单地遍历了一个列表然后调用了一些虚方法。不过,它和静态分配调用(statically dispatched call)比起来确实有一点慢,不过这一点除了在一些对性能要求极为苛刻的代码中,几乎是可以忽略不计的。

我认为观察者模式在性能消耗严重的代码路径之外使用是最为合适的,这样你就可以承担动态分配的消耗。撇开这个不谈的话,也找不到其他什么额外的消耗了。观察者模式没有给message分配新的对象,也没有队列。它只是在同步方法调用(synchronous method call)上存在了一点间接性。

“它太快了?”

实际上,你需要小心使用观察者模式,因为它是同步机制(synchronous)的。被观察者直接调用它的观察者们,这意味着在所有的观察者从它们的通知方法中返回之前,被观察者是不能继续它的工作的。一个速度慢的观察者会导致被观察者阻塞。
这听起来很可怕,但是在实践中,它也并不是世界末日。它只是一件你需要去注意的事情。UI程序员们--长久以来一直从事着这类基于事件(event-based)的编程工作--对这个问题有一句经过时间考验的座右铭:“远离UI线程”。

如果你在同步响应一个事件的话,你需要尽可能快地完成工作并回到操作线程,这样UI才不会锁住。如果你有一件比较慢的工作需要处理的话,把它丢到另一个线程或者工作队列中去处理吧。

然而,你必须小心地处理观察者们跟线程和显式锁(explicit locks)的关系。如果一个观察者尝试去抓取被观察者的线程锁的话,你的游戏就死锁了。在一个高度线程化的引擎中,使用事件队列(Event Queue)来进行异步通讯可能是一个更好的选择。

“它做了太多的动态分配”

整个程序员族群的部落--包括许多游戏开发者--已经迁移到了垃圾回收语言上,而动态分配也不再是曾经那个不靠谱的角色了。但是对于像游戏这样对性能要求很高的软件来说,内存分配始终存在问题,即使在托管语言(managed languages)中也是这样。动态分配在回收内存时是需要消耗时间的,尽管这是一个自动的过程。

许多游戏开发者对内存分配的担心要少于对碎片化的担心。当你的游戏需要进行连续几天的无崩溃运行去通过验收时,一个不断增加的碎片堆会阻止你的游戏的发售。
对象池这一章里会讨论到更多的细节, 并介绍一个避免碎片化的通用技术。

在之前的例子代码中,我使用了固定长度的数组,因为我想让事情看起来尽可能简单。在实际的实现中,观察者列表通常是一个动态分配的集合,大小会随着观察者的增减而变化。这里内存的变换着实困扰了一群人。

当然,首先需要注意的是,只有在连接新的观察者的时候内存才会被分配。发送通知是不需要进行内存分配的--它只是一个方法调用。如果你在游戏开始的时候就设置好所有的观察者,并且不会经常变动它们,内存的分配就会最小化。

如果仍然觉得有问题的话,下面我会介绍一个实现方法,这个方法里添加和删除观察者是没有任何动态内存分配的。

链接起来的观察者们

在之前的代码中,Subject被观察者拥有一个指向每一个监听它的观察者的指针列表。而Observer观察者类本身对这个列表没有做引用。它只是一个纯虚接口。接口要比具体的,状态类要更适合一些,所以这是一个好事情。

但是如果我们想给Observer类添加一些状态的话,可以通过观察者们自己组成被观察者的列表来解决分配问题。现在我们不再在Subject类中保存一个单独的指针集合,而是用一个Observer组成的链表来代替:

游戏编程设计模式 -- 观察者模式_第3张图片

为了实现这个结果,首先我们需要用一个指向观察者链表头的指针来取代Subject中的数组:

class Subject
{
  Subject()
  : head_(NULL)
  {}

  // Methods...
private:
  Observer* head_;
};

接下来,我们在Observer类中添加一个指向观察者列表中下一个观察者的指针:

class Observer
{
  friend class Subject;
public:
  Observer()
  : next_(NULL)
  {}

  // Other stuff...
private:
  Observer* next_;
}

在这里,我们还把Subject类声明成了友元类(friend class)。Subject类拥有添加和删除观察者的API,但是它所管理的列表现在嵌入了Observer类中。让被观察者可以控制这个列表最简单的方法就是把它变成观察者的友元类。

当需要注册一个新的观察者时,只要把它加入到列表中即可。我们选择最简单的方式,把新的观察者加入列表的头部:

void Subject::addObserver(Observer* observer)
{
  observer->next_ = head_;
  head_ = observer;
}

另外一个选择是插入链表的尾部。但是那么做的话会增加一定的复杂度。Subject需要去遍历整个列表去找到尾部或者保存一个单独的tail_指针去指向列表的最后一个节点。

添加到链表头部要更简单一些,不过也有一点副作用。当我们遍历列表去给每一个观察者发送通知时,最后注册的观察者会第一个收到消息。所以如果你以A、B、C的顺序注册观察者的话,它们收到通知的顺序是C、B、A。

理论上,这并没有什么影响。一个好的观察者设计需要遵循的一个原则是,两个监听同一个Subject的观察者,应该彼此之间没有顺序关系上的依赖。因为如果有顺序关系的话,就意味着这两个观察者会有一些微妙的耦合关系,而这样的耦合最终可能会对你的代码造成影响。

接下来让我们看看移除的功能:

void Subject::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 Subject::notify(const Entity& entity, Event event)
{
  Observer* observer = head_;
  while (observer != NULL)
  {
    observer->onNotify(entity, event);
    observer = observer->next_;
  }
}

这里我们遍历了整个列表,对列表中每一个观察者都发送了通知。这保证了所有的观察者都有相同的优先级,并且相互之间的独立的。
我们也可以这样修改:当一个观察者接收到通知后,会返回一个标识,告诉被观察者要不要继续遍历列表。如果你这么做的话,那你就很接近责任链(Chain of Responsibility)模式了

还不错,不是么?一个被观察者可以拥有无数个观察者,而不需要进行任何动态分配。注册和注销监听都和使用数组的时候是一样快的。不过我们牺牲了一个小特性。

因为我们使用的是观察者对象本身作为列表节点,这意味着它只能属于一个被观察者的观察者列表。也就是说,一个观察者在同一时间内只能监听一个被观察者。而在早前我们的实现中,每一个被观察者拥有自己的观察者列表时,每一个观察者是可以同时监听多个被观察者的。

你也许可以接受这个限制。而且我发现一个被观察者拥有多个观察者要比一个观察者同时监听多个被观察者要常见的多。如果这对你来说是个问题的话,还有另外一个更复杂的解决方案可以使用,这同样是没有动态分配的。这个方法在这一章里展开说的话就太长了,不过我可以把框架在这里介绍一下,剩下来的留给你们自己去补充...

列表节点池

让我们回到之前那个例子中那样,每一个Subject都保存了一个观察者的链表。不过,这个列表中的节点不再是观察者对象本身。取而代之的是,它们是一个独立的“列表节点”,包含了一个指向观察者的指针和列表中下一个节点的指针。

链表通常有两种实现形式。一种是你在学校里学到的那样,在列表节点对象中包含着数据。而在我们之前的例子中,这个情况倒过来了:数据(例子中的观察者Observer)包含了列表节点(next_ 指针)
后者被称为“介入式(intrusive)”链表,因为它把列表节点介入到了列表中实际需要包含的对象本身的定义中。这使得介入式链表缺少了一定的灵活性,但是,就像我们之前看到的那样,会有更高的效率。这种链表在像Linux系统内核这样的,值得用一定的灵活性去换取性能的地方,是非常流行的。

游戏编程设计模式 -- 观察者模式_第4张图片
observer-nodes.png

因为多个列表节点可以指向同一个观察者,这也就意味着一个观察者可以同时存在于多个被观察者的列表中。我们又回到了一开始可以同时监听多个被观察者的时候。

这里避免动态分配的方法也很简单:因为所有的列表节点都是相同的大小和类型,你可以预分配一个节点的对象池(object pool)。这样你就拥有了一定数量的节点去进行操作,你可以对它们进行使用和回收而不用任何内存重分配。

剩下来的问题

我想我们已经解决了这个模式可能会把人们吓跑的三个问题。就像我们看到的那样,现在它简单、快速并且在内存分配方面优化良好。但是这是不是意味着不管什么时候你都应该使用观察者模式呢?

这就是另外一个问题了。就像所有的设计模式那样,观察者模式也不是万灵药。即使正确并高效地实现,它也不一定是正确的解决方案。设计模式为人们所诟病的原因正是使用者把一个优秀的设计模式用在一个错误的地方,最终把事情变得更糟。

现在还剩下两个挑战需要解决,一个技术性的问题,另一个更偏向于可维护性的问题。我们先来解决技术性的问题,因为这总是最容易解决的。

销毁被观察者和观察者

之前的例子代码是可靠的,但是它回避了一个重要的情况:在你删除一个被观察者或者观察者时会发生什么?如果你草率地在观察者中调用delete方法,那么在被观察者中可能还留有了一个指向它的指针。这个指针现在是一个指向未分配内存的野指针。接下来当Subject尝试去发送通知的时候,好吧...我只能说你可能会变得不太开心。

并不是推卸责任,不过我不得不说设计模式确实没有提到这个情况。

删除Subject的时候要简单一些,因为在大多数的实现中,Observer是不会对它有任何引用的。但是即使这样,对Subject进行内存回收之后仍然可能引发一些问题。观察者们可能仍在期待能够接收到通知,但是它们不知道这永远不可能发生了。它们不再是观察者了,但它们认为自己还是。

你可以用很多方法来解决这个问题。最简单的就是像我接下来这样做。当观察者被删除时,从被观察者注销监听应该是观察者自己的工作。而通常,观察者是知道自己在监听哪个被观察者的,所以我们只需要在Observer的析构函数中添加一个removeObserver()调用就可以了。

通常的情况是,最难的部分不是如何去做,而是记得去做。

如果你不想在Subject销毁之后观察者们仍然在监听,也是很好解决的。只要让Subject在销毁时发送一个“濒死”通知。这样,每一个观察者都会收到这个消息,然后做它们认为合适的处理。

哀悼,送鲜花,组成挽歌,等等。

大多数人--即使是像我们这样已经工作了很多年的人--有的时候并不是那么可靠。这就是为什么我们发明了计算机:它们不会犯那些我们经常犯的错误。

一个更安全的解决方案是让观察者们在销毁时自动注销它们在监听的被观察者。如果你在代码库中实现了这个功能的话,后面使用这套代码的人就不需要再记得进行相关处理了。这可能会增加一定的复杂度,因为每个观察者都需要一个自己监听的被观察者列表。最终观察者和被观察者中都有了指向对方的指针。

不用担心,我有GC

你们这些使用带有垃圾回收机制的高级语言的家伙们现在一定觉得自鸣得意。你们觉得不需要去担心这个事情,因为你们从不显式地去删除任何东西?再好好想想!

想象这样一个场景:你有一个UI界面,用来显示玩家角色的血量、装备等信息。当玩家打开这个界面时,你创建了一个这个界面的对象。当玩家关闭界面时,你忘记去删除这个对象,让GC去进行回收处理。

每一次玩家被攻击到脸部(或者其他地方,我猜)的时候,角色会发出被攻击的通知。UI界面监听这个消息,然后更新HP血量条。好的,那么当玩家关闭这个界面,而你并没有注销这个UI界面的监听时会发生什么呢?

这个UI界面不再可见了,但是它也不会被垃圾回收机制处理,因为角色的观察者列表中仍然对它有引用。而每一次这个界面被加载时,我们都会去创建一个新的实例,这导致角色的观察者列表越来越长。

在玩家的整个游戏过程中,角色四处跑动,或者进入战斗,都会发送出通知,而这些通知会被创建出的所有的UI界面接收到。它们并不在屏幕上可见,但是它们会接收消息,并且浪费CPU资源去更新一些并不可见的UI元素。如果它们做了诸如播放声音之类的工作的话,你就会发现一些明显的错误了。

这在消息系统中是一个常见的情况,它有一个名字:失效监听器问题。以为Subject保留了对观察者们的引用,所以最后在内存中会存在一大堆的僵尸UI对象。从这里我们得到的教训是,需要妥善处理观察者的注销问题。

这个问题很重要的另一个更明显的标志是:它有一个Wikipedia页面。

发生了什么事?

另一个关于观察者模式更深层次的问题是由其设计目的引发的。我们使用观察者模式是为了介绍两个模块代码之间的耦合。它让Subject可以间接地和观察者通讯,而不是静态地绑定。

当你想要探究Subject的行为功能时,观察者模式是非常好的选择,它保证了没有任何额外的东西来对你进行干扰。当你想要研究物理引擎的时候,你一定不想你的编辑器或者你的大脑被一堆成就相关的东西所扰乱。

而另一方面,如果你的程序不能正常运行,并且bug分布于整个观察者链表中,这个时候去探究通讯流的话会变得更加困难。使用显式耦合时,想要找到被调用的方法会很容易。这对于IDE来说也是很容易完成的工作,因为耦合是静态的。

但是问题如果发生在观察者链表中的话,唯一的解决办法只能是找出运行中有哪些观察者在链表中。这时就不再有静态的通讯结构来供你探究,取而代之的是动态的通讯行为。

我对如何解决这个问题的指导方针是非常简单的。如果你需要经常去考虑通讯两侧的对象,才能更好地理解你的程序, 那么就不要用观察者模式来进行连接了。你可以用一些显式的关系来取代。

当你在研究一些大型项目的源代码时,你会发现它们都是有一些代码模块组成的。对这个我们有很多术语来表示,比如“关注分离(separation of concerns)”、“耦合和内聚(coherence and cohesion)”以及“模块化(modularity)”等。归根结底可以总结成一句话:“这些模块相互配合而并不相互依赖。”

观察者模式是让这些几乎没有关联的模块相互通讯而并不需要把它们整合到一个巨大的代码块中的最好方法。而它的作用在一个单一功能模块的代码中则要小得多。

这就是为什么观察者模式很适合于我们的例子:成就系统和物理引擎是两个几乎没有关联的模块,而且可能都是由不同的人来实现的。我们想要两者之间进行通讯时需要的关联尽可能的少,这样我们在各自单独的模块中工作时,并不需要知道太多另一个模块中的相关知识。

时至今日的观察者模式

设计模式诞生于1994年。那是,面向对象编程是最热门的编程范本。地球上的每一个程序员都想要“30天学会面向对象编程”,中层管理者们则基于程序员们创建的类来给他们发工资。工程师们则根据他们继承的层级数量来判断他们的能力。

同一年,Ace of Base有三首单曲上了畅销榜,这也许能让你了解一些我们那个年代的品味和欣赏水平。


因为水平有限,翻译的文字会有不妥之处,欢迎大家指正
“本译文仅供个人研习、欣赏语言之用,谢绝任何转载及用于任何商业用途。本译文所涉法律后果均由本人承担。本人同意平台在接获有关著作权人的通知后,删除文章。”

你可能感兴趣的:(游戏编程设计模式 -- 观察者模式)