观察者模式最佳实践,构建自己的一套事件分发系统

前言

试想这样一个问题,当某个事件发生时,比如在游戏中A模块修改了用户的金币数,而B模块和C模块提供的功能都依赖于用户的金币数,那么,A模块在修改金币数的同时,就需要通知B模块和C模块。常规的方法就是A模块持有B模块和C模块的对象,然后分别通过调用对象接口的方式告诉它们,“嘿,我修改了用户的金币数,改成了10金币”。
但这样就带来了许多问题:

  • A模块引用了B模块和C模块,耦合严重
  • A模块修改金币数的方法中调用了B,C模块的方法,当这两个模块发生变化时(比如B模块接收金币数的接口名称改变了,或是C模块不再需要知道金币数改变了),A模块也要修改
  • 当又出现一个D模块也需要知道金币数的变化时,同样需要修改A模块以适应这种需求

为了解决上面的问题,我们自然想到了观察者模式。

观察者模式

这里简单说一下什么是观察者模式:定义对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者(称之为观察者)都会接收到通知并自动更新。
观察者模式的好处是,对象之间是松耦合的,当一个对象改变状态时,它并不需要知道自己的观察者是谁,只需要发布通知即可。任何时候都可以增加或删除观察者,不会影响到发布通知的对象。
而事件分发系统就是观察者模式的一个具体实现

事件分发系统

事件分发系统核心需要提供的功能主要包括以下几个部分:

  • 当一个对象发生改变时,可以认为此时产生了一个事件,提供一个派发事件的接口,以通知所有的观察者
  • 需要提供注册监听事件的接口,以让观察者可以订阅自己需要接收的事件
  • 还需提供反注册监听事件接口,以让观察者可以取消自己的订阅
  • 最好还能在订阅的时候设置优先级,优先级越高的可以越先被通知

使用事件分发系统解决问题

首先,来看看使用事件分发系统处理上面提到的问题,会是什么样的效果。
A模块只需要派发金币修改事件,B,C模块只需要订阅金币修改事件,之后便可以收到通知了。是不是很简单呢

local B = class()
function B:on_money_change( money )
    print(money, "B receive event")
end
-- 订阅金币修改事件
EventSystem:on(Event.MoneyChanged, B.on_money_change, {target = B})

local C = class()
function C:on_money_change( money )
    print(money, "C receive event")
end
EventSystem:on(Event.MoneyChanged, C.on_money_change, {target = C})
-- 在A模块中派发金币修改事件,当前金币为10
EventSystem:emit(Event.MoneyChanged, 10)

接下来会仔细解读一下这个EventSystem事件分发系统的Lua实现代码。
实现事件分发系统时,需要小心一些特殊情况,比如有以下几个坑,读者可以留意一下代码中对这几个坑的处理

  • 在事件派发的过程中订阅该事件,订阅还有优先级,需要小心处理排序问题
  • 在事件派发的过程中取消订阅该事件,需要采用标记移除,不能直接移除
  • 在事件派发的过程中又派发了该事件,如何确定事件派发完成

为了便于讲解,下面的代码省略了一些非关键性的代码,用--- ...代替。

注册监听事件接口

function EventSystem:on( event, func, params )
    --- ...
    local event_listener = self._listeners[event]
    params = params or {}
    local priority = params.priority or 0
    local target = params.target
    --- ...
    local cb = {target = target, func = func, id = id, priority = priority}
    table.insert(event_listener.list, cb)
    id = id + 1
    if priority > 0 then
        event_listener.need_sort = true
        self:sort(event_listener)
    end
end

on方法中event参数表示要注册监听的事件名称,func参数表示当事件发生时要触发的回调函数,params表示额外参数,可以设置注册监听的目标target(可以利用它反注册所有与其相关的监听),也可以设置要注册监听的优先级,优先级越高的越先执行
on方法的实现还是比较简单的,主要就是将注册的相关信息插入到event_listener表中,但是明明注册的监听是有优先级的,却仍然只是调用table.insert将信息插入到表的末尾,这是为什么呢?读者可以先留意一下,后面会有详细解释。
还需要格外注意的是sort方法

function EventSystem:sort( listener )
    if listener.need_sort == true and listener.emit_count == 0 then
        table.sort(listener.list, function ( a, b )
            if a.priority == b.priority then
                return a.id < b.id
            else
                return a.priority > b.priority
            end
        end)
        listener.need_sort = false;
    end
end

可以看到sort方法必须在listener.emit_count == 0时才会进行排序,listener.emit_count == 0表示的是当前的事件没有处于派发状态,后面讲到派发接口时会详细解释,这里读者只需要知道其表示的含义即可。
事件处于派发状态时不能进行优先级排序原因是可能会造成回调的重复触发。
比如当前事件有4个回调 a, b, c, d,派发事件是顺序执行回调,当执行到第3个回调c时
如果在c回调中又注册了一个优先级最高的回调e,立刻排序的话,e插入到第一位,c会被挤到第4位,顺序执行到第4个回调时,导致c又被调用一次

反注册事件监听接口

function EventSystem:off( event, func, params )
    --- ...
    local event_listener = self._listeners[event]
    params = params or {}
    for i,cb in ipairs(event_listener.list) do
        if cb.func == func and cb.target == params.target then
            if event_listener.emit_count > 0 then
                -- 派发过程中只进行标记删除
                cb.need_remove = true
                event_listener.need_clean = true
            else
                table.remove(event_listener.list, i)
            end
            break;
        end
    end
end

off方法用于取消事件监听,当事件未处于派发过程中时,直接调用table.remove移除注册信息即可,但当事件处于派发过程中时,不能直接移除,只能先进行标记
在事件处于派发过程中时不能直接移除的原因是可能导致遗漏触发某些回调
比如当前事件有5个回调 a, b, c, d, e,顺序执行到第3个回调c时
如果在c回调中调用了off方法取消自己的监听,此时直接移除c的话,会导致d回调移动到第3位,e移动到第4位,顺序执行到第4个回调时,调用的是e而遗漏了d

事件派发接口

function EventSystem:emit( event, ... )
    --- ...
    local event_listener = self._listeners[event]
    local interrupt = false
    local length = #event_listener.list
    -- 这里不能使用ipairs,确保不会触发在派发过程中注册的事件
    -- 只取当前已经注册的事件数量,如果在派发过程中再注册(调用了table.insert),本次派发也不会调用
    for i = 1, length do
        if interrupt == true then
            break
        end
        local cb = event_listener.list[i]
        if cb.func and cb.need_remove ~= true then
            event_listener.emit_count = event_listener.emit_count + 1
            if cb.target then
                interrupt = cb.func(cb.target, ...)
            else
                interrupt = cb.func(...)
            end
            event_listener.emit_count = event_listener.emit_count - 1
        end
    end
    self:sort(event_listener);
    self:clean(event_listener);
    return interrupt
end

emit方法负责派发一个事件,顺序执行event_listener中注册的回调。事件的派发支持中断,当执行某个回调时,如果这个回调返回了true则可以中断当前事件的派发。
值得一提的是,代码通过对应的event_listener.emit_count = event_listener.emit_count + 1event_listener.emit_count = event_listener.emit_count - 1来记录事件的派发状态,当emit_count > 0则表明事件还在派发过程中。当emit_count == 0则表明事件派发完成。
不能使用event_listener.is_emiting = trueevent_listener.is_emiting = false代替的原因是如果在触发的回调中又派发了事件,形成了递归,那么二次派发事件结束时会直接将event_listener.is_emiting置为flase,导致一次派发事件对应的派发状态被标记错误

更多

事件分发系统的完整源码可以点击这里查看,测试用例可以点击这里查看
更多Lua相关的设计与使用,比如面向对象(代码中用到的class关键字),组件系统,分模块加载等等,可以查看GitHub仓库LuaKit

(0)

相关推荐

  • SpringBoot事件的监听与发布

    事件监听需要有事件本身,事件发布者,事件监听者三个重要的角色.需要使用事件监听解决业务,一般逃不过异步处理.降低耦合.随机触发等特点. 实现事件监听机制有四种常见的方法: 方法一:向Applicati ...

  • 【2/25】在Game上应用观察者模式(Observer Pattern)

    这是<小游戏从0到1设计模式重构>系列内容第2篇,所有源码及资料在"程序员LIYI"公号回复"小游戏从0到1"获取. 作者使用过的最简洁的观察者模式 ...

  • Spring 事件监听机制及原理分析

    简介 在JAVA体系中,有支持实现事件监听机制,在Spring 中也专门提供了一套事件机制的接口,方便我们实现.比如我们可以实现当用户注册后,给他发送一封邮件告诉他注册成功的一些信息,比如用户订阅的主 ...

  • Nat Chem|化学机器学习的最佳实践:推荐的一套标准化指南

    2021年6月,来自哥伦比亚大学的Nongnuch Artrith等人在Nature Chemistry上合作发表评论,为化学机器学习训练和报告的标准化推荐了一套指南. 以下是全文内容. 摘要 基于机 ...

  • 7种人力资源最佳实践

    什么是人力资源最佳实践? 最佳实践是一套通用的人力资源管理流程和行动.在人力资源管理研究中,有两种关于如何管理人员的思想流派:第一个是最合适的,第二个是最佳实践. 最合适的观点指出,为了增加价值,人力 ...

  • 培训的定义、作用和最佳实践

    一.什么是人力资源开发? 人力资源开发一词最早是在1969年提出的,指的是劳动力的培训,教育和发展.它旨在弥合学校教育和工作场所要求之间的差距. 在早期,HRD会进行严格的动手培训,重点是掌握硬技能. ...

  • 从最佳实践的焦虑中解脱出来

    几乎每天都会出现关于最佳实践的头条新闻.例如: 1.参加一个会议:"XX公司会议创新管理提高效率,我们现在没有." 2.行业文章:"人力资源部即将消失." 3. ...

  • 中国电信MEC最佳实践白皮书

    大数据.云计算.AI 等新一代信息技术的高速发展,在为新兴互联网行业提供强劲驱动之外,也在引领传统行业实施数字化.智能化转型,并催生出智能制造.智慧金融等一系列全新智能产业生态.在中国电信 MEC 平 ...

  • go-zero解读与最佳实践(上)

    本文有『Go开源说』第三期 go-zero 直播内容修改整理而成,视频内容较长,拆分成上下篇,本文内容有所删减和重构. 大家好,很高兴来到"GO开源说" 跟大家分享开源项目背后的一 ...

  • 160页PPT学习华为集成产品研发最佳实践之IPD(附下载)

    华为在整个企业内部改革中最重要的两个项目一个是ISC(集成供应链),另外一个就是IPD.用任正非的话讲,这两项改革关系到华为的生死存亡.其中IPD的项目是先行项目也是重点. 今天我们学习下华为的IPD ...

  • 远程软件工程师的10个最佳实践

    从表面上看,当考虑软件工程师研发效率的时候,我们可能会想到时间管理.沟通和任务完成的有效性.问题是完成任务或者有一个预期的时间表并不一定等同于生产力.对于远程工作的软件工程师而言,正面临着常规思考.责 ...

  • 雇主品牌最佳实践——UPS公益项目赋能雇主品牌创新实践

    能与年轻人产生共鸣的雇主品牌,一定不只有认知上的肯定,还有情感上的喜欢.当争夺优秀年轻人才取得竞争优势成为一种市场共识时,一个有态度的雇主品牌,如何运用差异化和情感上的打法,与同行同业形成区隔,成为了 ...