如何简化事件驱动代码的维护?


16

使用基于事件的组件时,在维护阶段我经常会感到有些痛苦。

由于执行的代码都是分散的,因此很难确定在运行时将涉及的所有代码部分。

当有人添加一些新的事件处理程序时,这可能导致难以解决的细微问题。

从评论中进行编辑:即使有一些良好的实践做法,例如具有应用程序范围的事件总线和处理程序将业务委派给应用程序其他部分的处理程序,有时代码也开始变得难以阅读,因为其中有很多来自许多不同地方的注册处理程序(尤其是在有巴士的情况下)。

然后,序列图开始查看复杂的情况,花费时间确定正在发生的事情在增加,调试会话变得混乱(在处理程序上迭代时,处理程序管理器上的断点,尤其是异步处理程序和在其之上进行一些过滤时感到高兴)。

/////////////
示例

我有一个正在检索服务器上某些数据的服务。在客户端上,我们有一个基本组件,该组件使用回调来调用此服务。为了向组件的用户提供扩展点并避免不同组件之间的耦合,我们触发了一些事件:一个事件在发送查询之前发生,一个事件在返回答案时发生,另一个事件在失败时发生。我们有一组预注册的基本处理程序,它们提供了组件的默认行为。

现在,该组件的用户(我们也是该组件的用户)可以添加一些处理程序以对行为进行一些更改(修改查询,日志,数据分析,数据过滤,数据按摩,UI花式动画,链接多个顺序查询) , 随你)。因此,某些处理程序必须在其他处理程序之前/之后执行,并且它们是从应用程序中许多不同的入口点注册的。

一段时间后,可能会发生十几个或更多处理程序被注册的情况,并且使用该处理程序可能既乏味又危险。

之所以出现这种设计,是因为使用继承开始变得一团糟。事件系统以一种组合形式使用,您尚不知道组合将是什么。

示例结束
//////////////

所以我想知道其他人如何处理这种代码。无论是在编写和阅读它。

您是否有任何方法或工具可让您轻松编写和维护此类代码?


您的意思是,除了从事件处理程序中重构逻辑之外
Telastyn

记录发生了什么。

@Telastyn,我不确定是否完全理解“除了从事件处理程序中重构逻辑之外”的意思。
纪尧姆

@Thorbjoern:看看我的更新。
Guillaume

3
听起来您使用的工具不正确吗?我的意思是,如果顺序很重要,那么您首先不应在应用程序的那些部分中使用普通事件。基本上,如果在典型的总线系统中对事件的顺序有所限制,那么它就不再是典型的总线系统了,但是您仍然照常使用它。这意味着您需要通过添加一些额外的逻辑来处理订单来解决该问题。至少,如果我正确地理解您的问题..
斯泰恩

Answers:


7

我发现使用内部事件堆栈(更具体地说,是具有任意删除的LIFO队列)处理事件极大地简化了事件驱动的编程。它使您可以将“外部事件”的处理分成几个较小的“内部事件”,并在它们之间具有明确定义的状态。有关更多信息,请参阅我对这个问题的回答

在这里,我给出一个简单的示例,该模式可以解决该问题。

假设您正在使用对象A来执行某些服务,并为其提供了回调以通知您完成的时间。但是,A使得在调用回调之后,它可能需要做更多的工作。当您在该回调中确定不再需要A并以某种方式销毁它时,就会产生危险。但是,从A调用您-如果在回调返回后A无法安全地确定它已被破坏,则在尝试执行其余工作时可能导致崩溃。

注意:的确可以用其他方式进行“销毁”,例如减少refcount,但这只会导致中间状态,以及处理这些状态的额外代码和错误。最好让A在不再需要之后完全停止工作,而不是继续处于某种中间状态。

在我的模式中,A可以通过将内部事件(作业)推送到事件循环的LIFO队列中,简单地安排它需要做的进一步工作,然后继续调用回调,并立即返回事件循环。这段代码不再是危险,因为A刚刚返回。现在,如果回调不破坏A,则推入的作业最终将由事件循环执行以做其额外的工作(在回调完成之后,并且递归所有推入的作业)。另一方面,如果回调确实销毁了A,则A的析构函数或deinit函数可以将推送的作业从事件堆栈中删除,从而隐式阻止了推送的作业的执行。


7

我认为适当的日志记录可以提供很大的帮助。确保将抛出/处理的每​​个事件都记录在某个地方(您可以为此使用日志记录框架)。调试时,可以查阅日志以查看错误发生时代码的确切执行顺序。通常,这确实可以帮助缩小问题原因。


是的,这是一个有用的建议。然后,产生的日志数量可能会令人恐惧(我记得在处理非常复杂的表单的事件中,我很难过一段时间才能找到循环的原因。)
Guillaume

也许一种解决方案是将有趣的信息记录到单独的日志文件中。另外,您可以为每个事件分配一个UUID,这样您就可以更轻松地跟踪每个事件。您还可以编写一些报告工具,使您可以从日志文件中提取特定信息。(或者,使用代码中的开关将不同的信息记录到不同的日志文件中)。
乔治

3

所以我想知道其他人如何处理这种代码。无论是在编写和阅读它。

事件驱动编程的模型在某种程度上简化了编码。它可能已经取代了旧的语言中使用的大型Select(或case)语句,并在早期的Visual开发环境(例如VB 3)中获得了普及(历史上不要引用我,我没有对其进行检查)!

如果事件序列很重要,并且在多个事件中分配了1个业务操作,该模型就会变得很痛苦。这种过程风格违反了这种方法的好处。不惜一切代价,尝试使动作代码封装在相应的事件中,并且不要从事件内部引发事件。这比起GoTo产生的意大利面条要糟糕得多。

有时,开发人员渴望提供需要此类事件相关性的GUI功能,但实际上并没有真正简单得多的替代方法。

最重要的是,如果使用得当,该技术还不错。


他们是否有其他替代设计来避免“比意大利面条差”?
Guillaume

我不确定是否有一种简单的方法可以不简化预期的GUI行为,但是如果您列出了所有与窗口/页面进行的用户交互,并且每个窗口都确定了程序将要执行的操作,则可以将代码分组到一个位置并直接进行一组负责实际处理请求的子方法的几个事件。另外,将仅用于GUI的代码与进行后端处理的代码分开可能会有所帮助。
NoChance 2012年

当我从继承地狱移到基于事件的东西时,重构的必要来自于重构。在某些方面,它确实更好,但在另一些方面,它却很糟糕……由于在某些GUI中“事件变得异常”我已经遇到了一些麻烦,我想知道如何做才能改善此类维护事件切片代码。
Guillaume

事件驱动的编程远比VB早。它存在于SunTools GUI中,在此之前,我似乎还记得它是内置在Simula语言中的。
凯文·克莱恩

@Guillaume,我想您已经过度设计了该服务。我上面的描述实际上主要是基于GUI事件。您(确实)需要这种类型的处理吗?
NoChance 2012年

3

我想更新此答案,因为自从“拉平”和“拉平”控制流程以来,我已经有了一些尤里卡的时刻,并对这个问题提出了一些新的想法。

复杂的副作用与复杂的控制流程

我发现我的大脑可以容忍通常在事件处理中发现的复杂的副作用类似图形的控制流,但不能容忍两者的结合。

如果将它们应用在非常简单的控制流中(例如顺序for循环),则可以很容易地推断出导致4种不同副作用的代码。我的大脑可以忍受一个顺序循环,该循环可以调整元素的大小和位置,对其进行动画处理,重新绘制它们并更新某种辅助状态。这很容易理解。

我同样可以理解复杂的控制流程,就像级联事件或遍历复杂的图形数据结构一样,如果在过程中顺序非常微小的副作用(例如标记元素)的作用很简单,在一个简单的顺序循环中以延迟方式进行处理。

当您拥有复杂的控制流程并导致复杂的副作用时,我会迷失,困惑和不知所措。在那种情况下,复杂的控制流程使您很难提前预测最终的结果,而复杂的副作用使得难以准确预测结果将以什么顺序发生。因此,正是这两件事的结合使它感到非常不舒服,即使现在代码完美运行,更改它也是如此令人恐惧,而不必担心会引起不必要的副作用。

复杂的控制流往往使人们难以推理何时/何地发生事情。如果这些复杂的控制流触发了副作用的复杂组合,而这对于了解何时/何处发生很重要,例如具有某种顺序依赖性的副作用,那么一件事情应该先发生在另一件事之前,那只会变得令人头疼。

简化控制流程或副作用

那么,当您遇到上述难以理解的情况时该怎么办?该策略是简化控制流程或副作用。

简化副作用的广泛应用策略是赞成延迟处理。以GUI调整大小事件为例,通常的诱惑可能是重新应用GUI布局,重新定位和调整子窗口小部件的大小,触发布局应用程序的另一级联以及调整层次结构的大小和位置,以及重新绘制控件,可能触发一些具有自定义大小调整行为的窗口小部件的唯一事件,这些事件触发更多事件导致“知道谁在哪里”等。与其一口气或通过散布事件队列来尝试全部完成,一种可能的解决方案是降低窗口小部件层次并标记哪些小部件需要更新其布局。然后,在以后的延迟传递中,该传递具有直接的顺序控制流程,为需要它的小部件重新应用所有布局。然后,您可以标记需要重新粉刷的小部件。再次在具有直接控制流的顺序延迟传递中,重新绘制标记为需要重绘的小部件。

这具有简化控制流和副作用的效果,因为控制流变得更简单,因为它在图遍历过程中没有级联递归事件。相反,级联发生在延迟的顺序循环中,然后可以在另一个延迟的顺序循环中进行处理。副作用变得很简单,因为在更复杂的类似于图形的控制流程中,我们所做的只是简单地标记需要延迟的顺序循环处理哪些触发更复杂的副作用的事情。

这确实会带来一些处理开销,但是,这可能会打开并行执行这些延期通行证的大门,如果您担心性能,则可能使您获得比开始时更有效的解决方案。通常,在大多数情况下,性能并不是什么大问题。最重要的是,尽管这看起来似乎没有什么区别,但我发现推理起来非常容易。它使预测发生的情况和时间变得更加容易,而且我不能高估能够更轻松地了解正在发生的事情的价值。


2

对我有用的是使每个事件独立存在,而无需参考其他事件。如果他们在asynchroniously来了,你不要一个序列,所以试图找出什么样的顺序是没有意义发生了什么,除了是不可能的。

最后要做的是一堆数据结构,这些数据结构被十几个线程以不特定的顺序读取,修改,创建和删除。您必须进行适当的多线程编程,这并不容易。您还必须考虑多线程,例如“在此事件中,我将查看特定时刻的数据,而不必考虑微秒前的数据,而无需考虑更改的时间” ,而不必考虑等待我释放锁的100个线程将要做什么。然后,我将基于此和所看到的情况进行更改。然后,我完成了。”

我发现自己正在做的一件事是扫描特定的Collection,并确保引用和collection本身(如果不是线程安全的)均已正确锁定并与其他数据正确同步。随着事件的增加,这种琐事也越来越多。但是,如果我跟踪事件之间的关系,那么琐事会变得更快。另外,有时很多锁定都可以用自己的方法进行隔离,实际上使代码更简单。

将每个线程视为一个完全独立的实体是困难的(因为使用硬核多线程),但是可行。“可伸缩”可能是我要找的词。两倍的事件仅需要两倍的工作量,而可能只有1.5倍。尝试协调更多异步事件将使您很快陷入困境。


我已经更新了我的问题。您对序列和异步内容完全正确。在处理所有其他事件之后必须执行事件X的情况下,您将如何处理?看起来我必须创建一个更复杂的处理程序管理器,但是我也想避免向用户暴露太多的复杂性。
Guillaume

在数据集中,有一个开关和一些在读取事件X时设置的字段。处理完所有其他开关后,您检查开关并知道必须处理X并且拥有数据。开关和数据实际上应该独立存在。设置好后,您应该认为“我必须完成这项工作”,而不是“我必须交X”。下一个问题:您如何知道事件已完成?如果您收到2个或更多事件X,该怎么办?最坏的情况是,您可以运行一个循环维护线程来检查情况并主动采取行动。?(3秒无输入X开关组然后运行关闭代码。
RalphChapin

2

听起来您正在寻找状态机和事件驱动的活动

但是,您可能还需要查看State Machine Markup Workflow Sample

这里是状态机实现的简短概述。一个状态机工作流程包括状态。每个状态由一个或多个事件处理程序组成。每个事件处理程序必须包含一个延迟或一个IEventActivity作为第一个活动。每个事件处理程序还可以包含SetStateActivity活动,该活动用于从一种状态转换为另一种状态。

每个状态机工作流都有两个属性:InitialStateName和CompletedStateName。创建状态机工作流的实例后,会将其放入InitialStateName属性。当状态机达到CompletedStateName属性时,它完成执行。


2
虽然从理论上讲这可以回答问题,但最好在此处包括答案的基本部分,并提供链接以供参考。
Thomas Owens

但是,如果每个状态都有数十个处理程序,那么在试图了解正在发生的事情时,情况就不会那么好。而且每个基于事件的组件都可能不会被描述为状态机。
Guillaume

1

事件驱动代码不是真正的问题。实际上,即使在明确定义了回调或使用内联回调的驱动代码中,遵循逻辑也没问题。例如,Tornado中的生成器样式回调非常容易遵循。

真正难以调试的是动态生成的函数调用。我会从地狱中调用回调工厂的(anti?)模式。但是,这种功能工厂在传统流程中同样难以调试。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.