游戏中的设计模式之 - Observer 模式

167 阅读4分钟

What we’d like, as always, is to have all the code concerned with one facet of the game nicely lumped in one place. The challenge is that achievements are triggered by a bunch of different aspects of gameplay. How can that work without coupling the achievement code to all of them? That’s what the observer pattern is for. It lets one piece of code announce that something interesting happened without actually caring who receives the notification.

observer 即发布订阅模式,将应用的某一部分逻辑抽象为 observer,然后以事件的形式将业务的发生广播给所有的订阅者。

由于 observer 会以同步的方式执行事件回调,这意味着在所有回调执行完成以前,observer 都无法重新开始工作,因此,应当编写尽可能高效的回调逻辑,以免出现阻塞。

在处理混合了 observer 和锁机制如多线程的程序时,最好使用事件队列来规避异步通信。

由于消费者订阅和取消订阅的过程在 observer 内部会引起动态内存分配,在某些对性能要求严苛的程序比如游戏中,开发者会对此有所顾虑,以下则是几种规避动态内存分配问题的方案:

链式 observer

image.png

我们在添加observer的时候只需要将当前subject的head替换为新的observer即可,这样就避免了可变数组引起的动态内存分配。

当删除一个observer的时候,则需要遍历这个 observer 链表,这可以利用双向链表来进行优化。

这种方案会导致的问题就是,一个observer只能同时属于一个subject,而原来使用数组的方案则不存在这种限制,这需要在设计上进行规避。

节点池

image.png

我们可以使用名为节点的数据结构存储observer的指针,从而实现让同一个observer同时监听多个subject。

避免动态分配的方法很简单:既然所有节点大小和类型都相同,您就可以预分配一个对象池。这样就可以获得一组大小固定的链表节点,根据需要使用和重复使用这些节点,无需调用实际的内存分配器。

仍然存在的问题

与其他的设计模式一样,observer并不适用所有的场景,如果对其的使用不恰当,很可能会让事情变糟。

销毁 subjects 和 observers

为了避免内存泄露和幽灵指针,我们需要在销毁observer的时候将其从subjects的事件接收者中删除,同时,在销毁subject时,也需要注销observer的监听事件,最好的实践是将这两块逻辑写在 subject和observer的内部实现中,以减少使用者的修改。

the lapsed listener problem

如果在注销事件监听后没有将subject中的observer对象清理,那么就会出现内存泄露。

observer list 导致的 debug 困难

与传统的显示耦合相比,由于observer本身的组织和设计问题引起的耦合更加隐秘,这种情况下应该尝试换一种更加明确的方式来表达这些逻辑之间的关系。

在开发某个大型程序时,往往需要同时处理很多代码。对于这种情况,我们有很多术语,如“关注点分离(separation of concerns)”、“内聚与耦合(coherence and cohesion)”和“模块化(modularity)”,但归根结底就是“这些东西放在一起,不要和其它东西混在一起”。

而 observer 模式在这种多模块互相通信的场景下非常好用。但是对于模块内部的逻辑实现,用处并不大。

observer 的未来

observer 是一种简单实用的古老的编程方式,使用这种方式会在各个模块中重复实现一系列的状态监听和事件通知的逻辑,针对这个问题,学术和工业界都在一直尝试创造新的模式来解决,比如“数据流驱动”,“响应式编程”等,这些解决方案往往专注于某个具体的领域,比如近几年在前端领域大热的数据绑定。

对多个模块之间的通信,observer 模式简单又好用,这是两个非常重要的优点。