杰哥瞎扯蛋之事件驱动机制
一楼先打个广告。之后我的分享贴基本会分为两个系列,瞎扯蛋和乱弹琴
瞎扯蛋是务虚谈原理谈原则的,一般想到就写,摸摸鱼就摸出来了。
乱弹琴比较务实,给出能用的代码,使用范例,和具体分析代码怎么写的。需要准备的东西比较多,属于要事先定计划的大项目。
第一篇乱弹琴的代码和测试都差不多了,就等补跨客户端兼容测试和文档了,预计顺利的话51期间能放出来。
坐等弹jj扯dd系列 一。为啥要事件驱动。
事件驱动,从根本来说,是为了解耦合,让代码与代码之间独立,可以分别编写/维护/规划。
以复杂度换自由度,是一种常见的降低心智负担的架构。
参考
[*]网页中的事件
[*]AWS的事件驱动架构
[*]百度百科
它易实现,使用广,自由度超过(甚至过高),在各种领域种都十分常见。
以mud为例
mudlet客户端都有一套自建的事件机制api
[*]参考
所以,了解事件驱动,使用事件驱动,对于构建一个易于维护和扩展的机器人是十分有价值的测试。
本帖最后由 jarlyyn 于 2024-4-26 03:23 PM 编辑
2.什么是事件驱动
事件驱动是一个很简单的模型,他包括几个概念
[*]事件(event):事件本质是一个key,用来表示 发生了什么。本质上只要独一无二就行,但为了可维护性,用有具体意义和负责模块的形式,比如'core.onConnected',可能是一种不错的实践
[*]上下文(context):上下文是一个很重要,但常常被忽视的编程概念。具体来说,你写的代码种涉及到的所有局部变量都可以认为是上下文。在解耦合的场景下,必须把上下文传递给处理函数,才能完成具体的工作。比如如果收到mud的行,触发事件'core.online',那必然要把具体接受到内容和事件一起传递,不然处理函数没法处理。
[*]处理函数(hanlder):处理函数就是具体用来工作的代码,一般会有一个接受上下文的参数,比如 function item:online(line) end。一般来说,如果架构允许的话,处理函数是可以有返回值用来和事件引擎交互的,比如是否继续处理。但出于mud的实际场景解耦合的目的,我不太建议这么做
[*]事件总线(event bus):事件总线是负责传递事件的机制。一般来说,处理函数会通过订阅/监听(Subscribe/Listen)关联到事件上,然后事件发生(raise event)时,总线会把事件广播(broadcast)给所有订阅过的处理函数进行处理。一般来说,一个hash表(lua的table)加几个简单的方法就能实现了。
以现实的例子来说。
比如中央精神的学习就是一个事件
中央开了最新的代表打会,为国家的发展指定了最新的路线图,这个学习的内容,也就是文件的内容,就是上下文。
各级机关的转发各下级就是事件总线
实际进行学习和总结的科室就是处理函数。
中央不需要关心具体某个子机关的科室是否需要学习文件,学习了怎么样。
各个科室不需要知道兄弟科室学习的状况,也不需要给中央反馈学习的成果。
不论科室的架构怎么调整,合并,开设。
只要各级机关的转发机制
中央的工作流程不需要了解科室怎么处理。
科室的工作也独立于其他处理相同工作的科室,不需要去关心。
这就是工作之间的互相解耦。
三。事件机制在mud里的应用。
首先,我们要整理下事件机制使用的场景。
正常情况下,如果你是在客户端的触发器,计时器,别名的send框的部分写机器人,是不需要事件机制的。
第一是复杂度不够,体现不出事件的优势。
要使用事件机制,你至少应该将自己的代码分成多个模块(科室),再有分别订阅和维护的必要。都在一起直接showhand就行,就是一个大藕,没有耦合可以解。
第二是触发器/计时器/别名本身就是一种事件处理函数了。
出现了匹配文字,到了预定事件,用户进行了特殊输入,是客户端收到的事件。
客户端在这个过程种相当于总线。
你在客户端里维护的内容,是相对应的处理函数。
在客户端里再写事件机制,大体相当于特地穿个成人纸尿裤去上大号,单纯折腾自己。
在常见的场景里,有价值的事件可以是
[*]断线('core.disconnected')和连线('core.disconnectged')。一般来断线后,会进行停止停止缓存输出等一系列处理。连线后也会进行一些初始化的确认工作
[*]进入房间('core.roomenter')事件。进入房间有多种形式的触发(下马车,爬山崖)等,进入房间后的操作,也根据你具体是遍历/行走/打架会有所不同。
[*]开始战斗('core.combatbegin')/结束战斗('core.combatend'。很多任务的重要流程就是结束战斗触发的。
[*]忙('core.busy')/不忙('core.nobusy').这个是一个很核心的事件,不忙后的选择太多了。
在模块可变(不停开发)和可选(不一定使用所有功能的情况下)
将这些mud中实际发生的事件抛给模块是一个很合理的方案。
如果全部写死,一旦mud的业务发生变化(举个例子,房间的描述调整顺序/结构)
使用事件系统,只要把抛事件的代码进行调整。
而如果写在一个个模块里的话,这工作量,属于一个小规模的人口普查了。
如果wiz比较勤快的话……你就天天改机器吧……
把mud的物理反馈(连线状态/行/gmcp)抽象成事件,能很好的降低维护的成本。
同时,使用事件驱动还有个附带的优势
不光代码,客户端想对于代码也脱耦了。
只要能实现同样的事件,客户端的跨大版本升级或者迁移到相对兼容的客户端才能具有可行性。
不然基本都要重写一遍。
四。事件机制的缺点
人月神话说过,没有银弹。
说人话就是没有万能的绝招。
按马克思的说法,就是生产关系还要适应具体的生产力呢。
事件机制最大的缺点就是它最大的优点,低耦合,高自由。
太不自由心智负担高,太自由了信之负担也会高。
首先,使用事件机制后,你不能依赖处理代码的顺序。
同一个事件,先进行A处理,再进行B处理,是不应该依赖的,哪怕框架支持,你也无法确保所有的模块都按你的意愿排序,所以无法强调顺序。
这个有个变通的妥协发案,就是个事件加入生命周期。
比如,响应mud的原始行的事件
'core.online'
可以演变为
'core.beforeonline','core.afteronline'。
如果还不够,那就需要'core.beforebeforeonline','core.afterafteronline',然后N个before和N个After,丑的一逼。
那个实际的例子,我的机器的初始化阶段的代码
App.Raise("BeforeInit")
App.Raise("Init")
App.Raise("InitMod")
App.Raise("Ready")
App.Raise("AfterReady")
App.Raise("Intro")
你就说丑不丑吧
其次,太过自由。
事件一但抛出,理论上它就是个孤儿,没人知道它从哪里来,没人知道它到哪里去。
说人话就是,一个事件,进入事件系统后,没法确认谁为发起(raise)事件负责,谁为处理(handle)事件处理。
谁都能发,谁都能处理,大家都是平等的,关系和国产狗血剧的感情关系图差不多。
最恐怖的是,你丢一个石头出去,不知道有多少院子里的狗会叫。
这个是事件系统本身最大优点的黑暗面,无法避免。
对这一点,我只能用两个方法来降低副作用。
第一是事件本身命名,都带上合适的作用域。
比如我一直举例都是 'core.'开头的事件。代表事件由core模块发起,非core模块不该发起这个事件。
第二是处理机制。
对于需要直接发出指令(对mud发送内容)的处理函数,
我用了一个伪状态机进行处理,确保对应事件最多只有一个处理函数进行处理
在这个模式下,将事件处理的优势缩小为 只有一个处理函数,或者被忽视不处理。
来降低复杂度。
绝大部分方案,都同时有优点,有缺点。
作为拿主意的人,只能做到,尽量利用足优点,尽量规避掉缺点
不管什么方案,都不是万能的。
最后,用伪代码实现下事件机制的简单实现来作为收尾吧。
先定一个全局变量作为总线
eventBus={}一个监听函数
function bindEvent(event,handler)
if eventBus==nil then
eventBus={}
end
eventBus.insert(handler)
end一个触发函数
function raiseEvent(event,context)
for i,v in ipairs(a) do
v(content)
end
end就实现了
再讲究点可以加个解绑函数
function unbindEvent(event,handler)
if eventBus==nil then
return
end
local result={}
for i,v in ipairs(eventBus) do
if v~=handler then
table.insert(result,v)
end
end
if (#result==0) then
eventBus=nil
else
eventBus=result
end
end利用函数可以通过地址是否想等判断来移除。
摸鱼完毕 虽然我看不懂中央文件,但不妨碍我学习贯彻
虽然我看不懂杰哥代码,但不妨碍我回复加精 代码的世界很神奇
页:
[1]
2