观察者 · 设计模式再探讨 · 游戏编程模式

内容

≡ 关于

§ 目录

在计算机上随便扔块石头都会砸到使用Model-View-Controller架构构建的应用程序,而底层则是观察者模式。观察者模式如此普遍,以至于Java将其放入其核心库(java.util.Observer),而C#则将其直接内置到了_语言_中(event关键字)。

就像软件中的许多事物一样,MVC是在上世纪七十年代由Smalltalk程序员发明的。Lisp程序员可能声称他们在六十年代就想出了这个概念,但并没有费心将其记录下来。

观察者是四人组模式中最广泛使用和广为人知的之一,但游戏开发世界有时可能会显得有些封闭,所以也许这对你来说都是新闻。如果你有一段时间没有离开修道院,让我通过一个激励性的例子来向你解释。

成就达成

说我们要在游戏中添加一个成就系统。它将包含数十种不同的徽章,玩家可以通过完成特定里程碑来获得,比如“杀死100只猴子恶魔”、“从桥上摔下来”或“只用一只死黄鼠狼完成一个关卡”

Achievement: Weasel Wielder

我发誓在我画这幅画时没有任何双关意思。

这个实现起来有些棘手,因为我们有各种各样的成就是通过各种不同的行为解锁的。如果我们不小心,成就系统的触角将纠缠在我们代码库的每一个角落。当然,“从桥上掉下来”与物理引擎有某种联系,但我们真的想在碰撞解决算法的线性代数中看到unlockFallOffBridge()的调用吗?

这是一个修辞问题。没有自尊的物理程序员会让我们用像“游戏玩法”这样俗气的东西玷污他们美丽的数学。

我们一如既往地希望,所有与游戏的某个方面有关的代码都能整洁地集中在一个地方。挑战在于成就是由游戏玩法的许多不同方面触发的。如何做到这一点,又不将成就代码与它们全部耦合在一起呢?

这就是观察者模式的作用。它允许一段代码宣布发生了有趣的事情,而不必关心谁接收到通知。

例如,我们有一些处理重力并跟踪哪些物体正在平稳表面上放松,哪些物体正在朝着必然毁灭的方向坠落的物理代码。要实现“从桥上掉下来”徽章,我们可以直接将成就代码塞进去,但那样会很混乱。相反,我们可以这样做:

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() 的参数由您决定。这就是为什么这是观察者 模式 而不是观察者“可以粘贴到您的游戏中的现成代码”。典型的参数是发送通知的对象和一个通用的“数据”参数,您可以将其他细节填入其中。

如果你在使用支持泛型或模板的编程语言,你可能会在这里使用它们,但也可以根据你的具体用例进行定制。在这里,我只是将其硬编码为接受一个游戏实体和描述发生了什么的枚举。

任何实现这个接口的具体类都将成为观察者。在我们的示例中,就是成就系统,所以我们会有类似这样的内容:

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; // 处理其他事件,并更新 heroIsOnBridge_... } } private: void unlock(Achievement achievement) { // 如果尚未解锁,则解锁... } bool heroIsOnBridge_; };

主题

通知方法由被观察的对象调用。按照四人帮的说法,该对象被称为“主题”。它有两个职责。首先,它保存着那些耐心等待它的消息的观察者列表:

类 主题 { private: 观察者* 观察者_[最大观察者数]; int 观察者数量_; };

在真实的代码中,你会使用一个动态大小的集合,而不是一个愚蠢的数组。我在这里坚持基础知识,适用于那些不熟悉C++标准库的其他语言背景的人。

重要的是,该主题公开了一个用于修改该列表的 API:

class Subject { public: void addObserver(Observer* observer) { // Add to array... } void removeObserver(Observer* observer) { // Remove from array... } // Other stuff... };

这允许外部代码控制谁接收通知。主题与观察者进行通信,但它并不与它们耦合。在我们的示例中,没有一行物理代码会提到成就。然而,它仍然可以与成就系统通信。这就是这种模式的巧妙之处。

主题拥有观察者列表而不是单个观察者也很重要。这样可以确保观察者之间不会隐式耦合。例如,假设音频引擎还观察下落事件以便播放适当的声音。如果主题只支持一个观察者,当音频引擎注册自己时,就会注销成就系统。

这意味着这两个系统会相互干扰,而且以一种特别恶劣的方式,因为第二个会禁用第一个。支持观察者列表可以确保每个观察者都被独立对待。就他们所知,每个观察者都是世界上唯一关注该主题的事物。

主题的另一个工作是发送通知:

类 主题 { 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); };

这样我们可以将 Subject 中的 notify() 设置为 protected。这样派生的物理引擎类可以调用它发送通知,但外部代码则无法。同时,addObserver()removeObserver() 是公共的,因此任何可以访问物理系统的东西都可以观察它。

在真实的代码中,我会避免在这里使用继承。相反,我会让Physics 拥有 Subject 的一个实例。观察物理引擎本身的代替方案是,主题将是一个单独的“下落事件”对象。观察者可以使用类似以下方式注册自己:

physics.entityFell() .addObserver(this);

对我来说,这就是“观察者”系统和“事件”系统之间的区别。在前者中,你观察_做了某些有趣事情的东西_。而在后者中,你观察代表_发生有趣事情的对象_。

现在,当物理引擎执行一些值得注意的操作时,就会像之前的示例中那样调用 notify()。这会遍历观察者列表并通知它们。

A Subject containing a list of Observer pointers. The first two point to Achievements and Audio.

相当简单,对吧?只需一个类来维护指向某个接口实例的指针列表。很难相信如此直接的东西是无数程序和应用框架的通信基础。

但观察者模式并非没有批评者。当我询问其他游戏程序员对这种模式的看法时,他们提出了一些抱怨。让我们看看是否有什么可以做来解决这些问题。

我经常听到这样的说法,通常是来自那些实际上并不了解这种模式细节的程序员。他们默认认为,任何类似“设计模式”的东西一定涉及大量类和间接性,以及其他浪费 CPU 周期的创造性方式。

观察者模式在这里特别不受欢迎,因为它已知会与一些名为“事件”、“消息”甚至“数据绑定”的可疑角色交往。其中一些系统可能会很慢(通常是故意的,也是有充分理由的)。它们涉及排队或为每个通知执行动态分配等操作。

这就是我认为记录模式很重要的原因。当我们对术语模糊不清时,我们失去了清晰简洁地沟通的能力。你说“观察者”,有人却听成了“事件”或“消息”,因为要么没有人费心写下区别,要么他们碰巧没有读到。

我尝试用这本书来做到这一点。为了全面,我还有一个关于事件和消息的章节:事件队列

但是,现在你已经看到了模式是如何实现的,你知道情况并非如此。发送通知只是遍历列表并调用一些虚拟方法。诚然,这比静态分派调用慢一点,但在除了性能至关重要的代码之外,这个成本是可以忽略的。

我发现这种模式最适合在热代码路径之外,因此通常可以承受动态分派。除此之外,几乎没有额外开销。我们不为消息分配对象。没有排队。这只是对同步方法调用的间接引用。

事实上,你必须小心,因为观察者模式 同步的。主题直接调用其观察者,这意味着在所有观察者从其通知方法返回之前,主题不会恢复自己的工作。一个慢的观察者可能会阻塞主题。

这听起来很可怕,但实际上并不是世界末日。这只是你需要注意的一件事。UI程序员们,他们已经做了很多年这样的基于事件的编程,对此有一个久经考验的座右铭:“远离UI线程”.

如果您正在同步响应事件,则需要尽快完成并返回控制权,以避免 UI 卡死。当您需要执行耗时操作时,请将其推送到另一个线程或工作队列。

在混合观察者、线程和显式锁时,你必须小心。如果观察者试图获取主体拥有的锁,可能会导致游戏死锁。在高度线程化的引擎中,最好使用异步通信,使用事件队列

整个程序员部落的许多成员,包括许多游戏开发者,已经转向了垃圾收集语言,动态分配并不像过去那样可怕。但对于像游戏这样对性能要求很高的软件来说,内存分配仍然很重要,即使在托管语言中也是如此。动态分配需要时间,回收内存也需要时间,即使这些过程是自动进行的。

许多游戏开发者更担心的是碎片化,而不是分配。当你的游戏需要连续运行数天而不崩溃才能获得认证时,日益碎片化的堆可能会阻止你发货。

对象池》章节详细介绍了这一点,以及避免这种情况的常见技术。

在之前的示例代码中,我使用了一个固定数组,因为我试图保持事情非常简单。在实际实现中,观察者列表几乎总是一个动态分配的集合,随着观察者的添加和移除而增长和缩小。这种内存波动使一些人感到不安。

当然,首先要注意的是,只有在连接观察者时才会分配内存。 发送通知根本不需要任何内存分配 - 它只是一个方法调用。 如果在游戏开始时连接观察者并且不经常干扰它们,分配的内存量是最小的。

如果仍然存在问题,我将介绍一种在完全不进行动态分配的情况下实现添加和移除观察者的方法。

在我们迄今为止看到的代码中,Subject 拥有一个指向每个观察者的指针列表。Observer 类本身没有引用这个列表。它只是一个纯虚接口。接口优于具体的、有状态的类,所以这通常是一件好事。

但是,如果我们愿意在Observer中放入一点状态,我们可以通过将主题的列表直接通过观察者本身来解决我们的分配问题。主题不再拥有单独的指针集合,观察者对象成为链表中的节点:

A linked list of Observers. Each has a next_ field pointing to the next one. A Subject has a head_ pointing to the first Observer.

要实现这一点,首先我们将去掉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 设为友元类。主题拥有添加和移除观察者的 API,但它将管理的列表现在在 Observer 类内部。让它能够访问该列表的最简单方法是将其设为友元。

注册一个新的观察者只是将其连接到列表中。我们将选择简单的选项,并将其插入到最前面:

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

另一种选择是将其添加到链表的末尾。这样做会增加一些复杂性。Subject 必须要么遍历列表找到末尾,要么保持一个单独的 tail_ 指针,始终指向最后一个节点。

将其添加到列表的开头更简单,但确实会产生一个副作用。当我们遍历列表以向每个观察者发送通知时,最近注册的观察者会首先收到通知。因此,如果按顺序注册观察者 A、B 和 C,它们将按照 C、B、A 的顺序接收通知。

从理论上讲,这并不重要。良好观察者纪律的原则是,观察同一主题的两个观察者之间不应该存在相互的排序依赖关系。如果排序确实重要,那意味着这两个观察者之间存在某种微妙的耦合,可能会给你带来麻烦。

让我们开始移除工作:

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

在这里,我们遍历整个列表并通知其中的每个观察者。这确保所有观察者都具有相同的优先级且彼此独立。

我们可以调整这样的方式,当观察者被通知时,它可以返回一个标志,指示主题是否应继续遍历列表或停止。如果你这样做,你就很接近拥有责任链模式

还不错,对吧?一个主题可以有尽可能多的观察者,而不会有一丝动态内存。注册和注销的速度与简单数组一样快。不过,我们牺牲了一个小功能。

由于我们将观察者对象本身用作列表节点,这意味着它只能是一个主题的观察者列表的一部分。换句话说,观察者一次只能观察一个主题。在更传统的实现中,每个主题都有自己独立的列表,观察者可以同时存在于多个主题中。

你可能能够接受这种限制。我发现主题拥有多个观察者比反过来更常见。如果这对你是个问题,你可以使用另一种更复杂的解决方案,它仍然不需要动态分配。这个解决方案太长,无法在本章节内详细介绍,但我会简要概述并让你填写细节...

与以往一样,每个主题都将有一个观察者的链表。然而,这些链表节点不会是观察者对象本身。相反,它们将是单独的小“链表节点”对象,其中包含一个指向观察者的指针,然后是指向列表中下一个节点的指针。

A linked list of nodes. Each node has an observer_ field pointing to an Observer, and a next_ field pointing to the next node in the list. A Subject's head_ field points to the first node.

由于多个节点可以同时指向同一个观察者,这意味着一个观察者可以同时存在于多个主题的列表中。我们又可以同时观察多个主题了。

链表有两种形式。在学校学到的一种中,有一个包含数据的节点对象。在我们之前的链表观察者示例中,情况有所不同:数据(在这种情况下是观察者)包含了_节点_(即 next_ 指针)。

后一种风格被称为“侵入式”链表,因为在列表中使用对象会侵入到对象本身的定义中。这使得侵入式链表不太灵活,但正如我们所见,也更高效。它们在像 Linux 内核这样的地方很受欢迎,因为这种权衡是有意义的。

避免动态分配的方法很简单:由于所有这些节点的大小和类型都相同,您可以预先分配一个对象池。这样就为您提供了一个固定大小的列表节点堆,您可以根据需要使用和重复使用它们,而无需访问实际的内存分配器。

我认为我们已经消灭了用来吓唬人们远离这种模式的三个鬼怪。正如我们所看到的,它简单、快速,并且可以很好地与内存管理协作。但这是否意味着你应该始终使用观察者?

现在,这是一个不同的问题。就像所有设计模式一样,观察者模式并非万能之策。即使正确高效地实现了,它也可能不是正确的解决方案。设计模式声名狼藉的原因是因为人们将好的模式应用于错误的问题上,结果变得更糟。

仍然存在两个挑战,一个是技术性的,另一个则更像是可维护性层面上的。我们将先解决技术性的挑战,因为这些通常是最容易的。

我们演示的示例代码很稳固,但它回避了一个重要问题:当你删除一个主题或一个观察者时会发生什么?如果你粗心地在某个观察者上调用 delete,主题可能仍然持有指向它的指针。这就是一个悬空指针指向已释放的内存。当该主题尝试发送通知时,嗯...让我们说你将度过不愉快的时光。

不是在指责别人,但我要指出《设计模式》根本没有提到这个问题。

销毁主题更容易,因为在大多数实现中,观察者没有任何对它的引用。但即便如此,将主题的位发送到内存管理器的回收站可能会引起一些问题。这些观察者可能仍然期望在未来收到通知,而他们并不知道现在永远不会发生了。实际上,他们根本不是观察者,只是认为自己是。

你可以用几种不同的方式来处理这个问题。最简单的方法就是像我一样,直接放弃它。当观察者被删除时,它的工作就是从任何主题中注销自己。观察者通常知道自己正在观察哪些主题,所以通常只需要在析构函数中添加removeObserver()调用即可。

通常情况下,困难的部分不是去做,而是记得去做。

如果你不想让观察者在主体离开时一筹莫展,那很容易解决。只需让主体在被销毁之前发送最后一条“临终通知”。这样,任何观察者都可以收到并采取它认为合适的行动。

哀悼,送花,写挽联等。

人类——即使是我们中那些与机器相处时间足够长,以至于有些机器的精确特性潜移默化在我们身上的人——在可靠性方面总是糟糕的。这就是为什么我们发明了计算机:它们不会犯我们经常犯的错误。

更安全的做法是当观察者被销毁时,让它们自动从每个主题中注销。如果你在基础观察者类中实现了这个逻辑,那么每个使用它的人都不必记得自己去做。不过,这确实增加了一些复杂性。这意味着每个观察者都需要一个它正在观察的主题列表。最终你会得到指针双向指向的情况。

所有你们那些使用带有垃圾回收器的时髦现代语言的酷孩子们现在感到相当自鸣得意。认为你们不必担心这个问题,因为你们从未明确删除任何东西?再想想吧!

想象一下:你有一个 UI 屏幕,显示玩家角色的各种统计数据,比如他们的生命值等等。当玩家打开屏幕时,你为其实例化一个新对象。当他们关闭它时,你只需忘记这个对象,让垃圾回收器清理它。

每次角色被打在脸上(或其他地方,我猜),都会发送通知。UI屏幕观察到这一点并更新小健康条。很好。现在如果玩家关闭屏幕,但你没有取消注册观察者,会发生什么?

UI不再可见,但由于角色的观察者列表仍然引用它,因此它不会被垃圾回收。每次加载屏幕时,我们都会将一个新实例添加到那个越来越长的列表中。

玩家在玩游戏、四处奔跑、参与战斗的整个过程中,角色会发送通知,这些通知会被所有这些屏幕接收到。这些通知不会显示在屏幕上,但它们会接收通知并浪费 CPU 周期更新不可见的 UI 元素。如果它们执行其他操作,比如播放声音,你会注意到明显的错误行为。

这在通知系统中是一个常见问题,它有一个名字:失效的监听器问题。由于主题保留对其监听器的引用,可能会导致僵尸 UI 对象残留在内存中。这里的教训是要有纪律地进行注销。

它的重要性更为明显的迹象是:它有一个维基百科文章

观察者模式的另一个更深层次的问题是其预期目的的直接结果。我们使用它是因为它帮助我们减少两段代码之间的耦合。它使一个主题能够间接地与某个观察者通信,而不会被静态地绑定在一起。

当你试图推理主体的行为时,这真的是一个真正的胜利,任何附庸都会成为一个讨厌的干扰。如果你在研究物理引擎,你真的不希望你的编辑器或你的思绪被一堆关于成就的东西淹没。

另一方面,如果您的程序出现问题,并且错误涉及一系列观察者,那么推理该通信流程就会变得更加困难。有了显式耦合,只需查找被调用的方法就很容易。对于您的平均集成开发环境来说,这是小菜一碟,因为耦合是静态的。

但如果这种耦合是通过观察者列表发生的,唯一能够告知谁会收到通知的方法是查看在运行时存在于该列表中的观察者。无法对程序的通信结构进行静态推理,而必须推理其命令式、动态行为。

我处理这个问题的指导原则非常简单。如果你经常需要同时考虑某种通信的两个方面才能理解程序的一部分,就不要使用观察者模式来表达这种联系。最好选择更明确的方式。

当你在开发一个大型程序时,你往往会一起处理其中的一部分。我们有很多术语来描述这种情况,比如“关注点分离”、“连贯性和内聚性”以及“模块化”,但归根结底就是“这些东西放在一起,而不与其他东西混在一起”。

观察者模式是一种很好的方法,让那些大部分不相关的块彼此交流,而不会融合成一个大块。在专门用于一个功能或方面的代码块内部,它的用处就不那么大了。

这就是为什么它很适合我们的例子:成就和物理几乎完全不相关,很可能由不同的人实现。我们希望它们之间的沟通最少,这样在任何一个领域工作都不需要太多了解另一个领域。

设计模式 于1994年问世。那时,面向对象编程是当时最流行的范式。地球上的每个程序员都想“30天学会面向对象编程”,中层管理者根据他们创建的类的数量来支付报酬。工程师通过他们继承层次结构的深度来评判自己的能力。

那一年,Ace of Base 不仅有一首而是三首热门单曲,这或许能说明当时我们的品味和洞察力。

观察者模式在那个时代变得流行,所以它以类为主也就不足为奇了。但现在主流的编程人员更喜欢函数式编程。为了接收通知而不得不实现整个接口,已经不符合当今的审美观。

它感觉沉重而僵硬。它确实沉重而僵硬。例如,您不能有一个单一的类,为不同的主题使用不同的通知方法。

这就是为什么主题通常将自身传递给观察者。由于观察者只有一个 onNotify() 方法,如果它正在观察多个主题,它需要能够知道是哪个主题调用了它。

更现代的方法是将“观察者”仅作为对方法或函数的引用。在具有一级函数的语言中,尤其是具有闭包的语言中,这是一种更常见的观察者模式。

这些天,几乎 每种 语言都支持闭包。C++ 在没有垃圾回收的语言中克服了闭包的挑战,甚至 Java 最终也在 JDK 8 中引入了它们。

例如,C# 中内置了“事件”。在其中,您注册的观察者是一个“委托”,这是该语言对方法引用的术语。在 JavaScript 的事件系统中,观察者可以是支持特殊 EventListener 协议的对象,但它们也可以只是函数。后者几乎总是人们使用的方式。

如果我今天要设计一个观察者系统,我会选择基于函数而不是基于类。即使在 C++ 中,我也更倾向于一个让你注册成员函数指针作为观察者而不是某个 Observer 接口实例的系统。

这里有一篇关于在C++中实现这一点的有趣博客文章。

观察者明天

事件系统和其他类似观察者模式的模式在当今非常普遍。它们是一条经过深思熟虑的路径。但是,如果你使用它们编写了一些大型应用程序,你会开始注意到一些事情。你的观察者中的许多代码最终看起来都很相似。通常是这样的:

  1. 收到某个状态已更改的通知。
  1. 强制性地修改某个 UI 块以反映新状态。

这一切都是,“哦,英雄的生命值现在是7?让我把生命值条的宽度设置为70像素。” 过了一会儿,这变得相当乏味。 计算机科学学者和软件工程师一直在努力消除这种乏味已经很长时间了。 他们的尝试以许多不同的名称出现过:“数据流编程”,“函数响应式编程”等。

虽然在一些领域取得了一些成功,通常是在有限的领域,比如音频处理或芯片设计,但圣杯仍未被发现。与此同时,一种不那么雄心勃勃的方法开始受到关注。许多最近的应用框架现在使用“数据绑定”技术。

与更激进的模型不同,数据绑定并不试图完全消除命令式代码,也不试图围绕一个巨大的声明式数据流图构建整个应用程序架构。它所做的是自动化繁琐的工作,当您调整 UI 元素或计算属性以反映某个值的变化时。

与其他声明性系统一样,数据绑定可能过于缓慢和复杂,无法完全融入游戏引擎的核心。但如果我没有看到它开始在游戏的非关键领域(如用户界面)取得进展,我会感到惊讶。

与此同时,那个古老的观察者模式仍然在这里等待我们。当然,它并不像某些炙手可热的技术那样令人兴奋,能够在其名称中同时包含“功能性”和“响应式”,但它非常简单且有效。对我来说,这些通常是解决方案最重要的两个标准。

≡ 关于

§ 目录

总结
这篇文章介绍了观察者模式在软件开发中的应用。观察者模式是一种常见的设计模式,用于实现对象之间的解耦,让一个对象在状态发生变化时通知其他对象而无需关心具体接收者。文章以游戏开发中的成就系统为例,说明了如何使用观察者模式实现成就的解耦和灵活性。具体实现包括定义观察者接口和主题类,以及在物理引擎中发送通知并让成就系统接收。通过观察者模式,不同模块之间可以实现松耦合,使系统更易于维护和扩展。