事件驱动的编程:什么时候值得?


19

好的,我知道这个问题的标题几乎与何时应该使用基于事件的编程相同?但是上述问题的答案并没有帮助我确定我是否应该在遇到的特定情况下使用事件。

我正在开发一个小型应用程序。这是一个简单的应用程序,大部分功能是基本CRUD。

在发生某些事件时(修改某些数据时),应用程序必须在文件中写入所述数据的本地副本。我不确定实现此目标的最佳方法是什么。我可以:

  • 修改数据时触发事件,并将响应(生成文件)绑定到此类事件。或者,实现观察者模式。这似乎是不必要的复杂性。
  • 直接从修改数据的代码中调用文件生成代码。简单得多,但是依赖关系应该是这种方式似乎是错误的,也就是说,将应用程序的核心功能(修改数据的代码)与额外的特权(生成备份文件的代码)耦合起来似乎是错误的。我知道,但是,这个应用程序不会发展到那种耦合带来问题的地步。

在这种情况下最好的方法是什么?


2
我会说自己什么都不做-只需使用现有的事件总线即可。这将使生活变得更加简单...
蜘蛛侠鲍里斯(Boris)

事件驱动编程本质上是异步的。事件可能会或可能不会按照您想要的顺序进行,或者可能会以其他顺序进行,或者根本不会进行。如果您可以处理这种额外的复杂性,那就去做。
Pieter B

事件驱动通常意味着将您的代码作为回调提供,并且以无法预测的方式从其他位置调用这些回调。您的描述听起来更像是当代码中发生某些特定的事情时,您需要做的比天真的实现还要多。只需输入额外的电话即可。
ThorbjørnRavn Andersen

这里事件驱动和基于事件的之间的差异。参见例如与Ted Faison一起发人深省的.NET Rocks播客第355集Ted Faison将活动发挥到极致!直接下载URL)和《基于事件的编程》一书:最大限度地限制事件
Peter Mortensen

Ted Faison的采访开始于13分10秒。
Peter Mortensen

Answers:


31

遵循KISS原则:保持简单,愚蠢或YAGNI原则:您不需要它。

您可以编写如下代码:

void updateSpecialData() {
    // do the update.
    backupData();
}

或者您可以编写如下代码:

void updateSpecialData() {
     // do the update.
     emit SpecialDataUpdated();
}

void SpecialDataUpdatedHandler() {
     backupData();
}

void configureEventHandlers() {
     connect(SpecialDataUpdate, SpecialDataUpdatedHandler);
}

如果没有必要的其他理由,请按照更简单的方法进行。诸如事件处理之类的技术功能强大,但是它们增加了代码的复杂性。它需要更多代码才能工作,这使得代码中发生的事情更难以理解。

在正确的情况下,事件是非常关键的(想像在没有事件的情况下尝试进行UI编程!),但是当可以使用KISS或YAGNI时,请不要使用它们。


我特别喜欢您提到在更改数据时触发事件并非无关紧要的事实。
NoChance

13

您描述的示例简单数据,其中的修改触发了某些效果,可以使用观察者设计模式完美实现:

  • 这比完整事件驱动的代码更易于实现和维护。
  • 主题和观察者之间的耦合可以是抽象的,这有助于分离关注点。
  • 这是一对多关系的理想选择(对象具有一个或多个观察者)。

事件驱动的方法值得在更复杂的场景中进行投资,在多对多的情况下可能会发生许多不同的交互作用,或者在设想了连锁反应的情况下(例如,主体告知观察者,在某些情况下,该观察者想要修改科目或其他科目)


1
我很困惑,观察者不是实现事件的一种方法吗?
2013年

1
@svick我不这么认为。在事件驱动的编程中,您有一个主循环,该循环使用发送者和观察者分离的多对多关系处理事件。我认为观察者可以通过处理特定类型的事件来做出贡献,但是仅靠观察者您就无法获得完整的EDP。我认为混淆来自以下事实:在事件驱动的软件中,观察者有时是在事件处理的顶部实现的(通常是带有GUI的MVC)
Christophe

5

如您所说,事件是减少类之间耦合的好工具。因此,尽管它可能涉及在不内置对事件的支持的情况下以某些语言编写其他代码,但它降低了总体情况的复杂性。

事件可以说是OO中最重要的工具之一(根据Alan Kay的说法- 对象通过发送和接收消息进行通信)。如果您使用对事件有内置支持的语言,或者将函数视为头等公民,那么使用它们就可以了。

即使在没有内置支持的语言中,类似Observer模式的样板数量也非常少。您也许可以在某个地方找到一个不错的通用事件库,在所有应用程序中使用该库以最小化样板。(通用事件聚合器或事件中介器在几乎任何类型的应用程序中都很有用)。

在小型应用程序中值得吗?我肯定会说是的

  • 使类彼此分离,可以使类依赖关系图保持干净。
  • 可以单独测试没有任何具体依赖性的类,而无需考虑测试中的其他类。
  • 没有任何具体依赖关系的类需要更少的单元测试来完整覆盖。

如果您在考虑“哦,但这实际上只是一个很小的应用程序,它并不重要”,请考虑:

  • 小型应用程序有时最终会在以后与大型应用程序结合在一起。
  • 小型应用程序可能至少包含某些逻辑或组件,以后可能需要在其他应用程序中重用这些逻辑或组件。
  • 小型应用程序的需求可能会发生变化,从而引发了重构的需求,当将现有代码分离时,这将变得更加容易。
  • 以后可以添加其他功能,提示需要扩展现有代码,而当现有代码已经解耦时,这也容易得多。
  • 通常,松耦合的代码比紧耦合的代码花费的时间更少。但是紧密耦合的代码比松散耦合的代码需要更长的重构和测试时间。

总体而言,应用程序的大小不应成为是否保持类之间松散耦合的决定因素;SOLID原则不仅适用于大型应用程序,还适用于任何规模的软件和代码库。

实际上,在对松散耦合的类进行隔离的单元测试中节省的时间应该抵消在解耦这些类上花费的任何额外时间。


2

观察者模式的实现方式可以比Wikipedia文章小得多。假设您的编程语言支持“回调”或“代理”之类的内容,则(或GOF书)描述的。只需将回调方法传递到您的CRUD代码中即可(观察者方法,该方法可以是通用的“写入文件”方法,也可以是空方法)。代替“事件触发”,只需调用该回调即可。

与直接调用生成文件的代码相比,生成的代码仅具有最小的复杂性,但是没有紧密相关的组件的缺点。

这将带给您“两全其美”,而无需牺牲“ YAGNI”的去耦。


Doc在第一次使用时有效,但是当事件需要触发第二件事时,很可能需要以这种方式修改类。如果使用观察者模式,则可以根据需要添加任意数量的新行为,而无需打开原始类进行修改。
RubberDuck

2
@RubberDuck:OP一直在寻找一种“避免不必要的复杂性”的解决方案-如果需要不同的事件/不同的行为,他可能不会认为观察者模式过于复杂。所以我同意你的意见,当事情变得更加复杂时,一个完整的观察员会更好地为他服务,但只有那时。
布朗

一个公平的声明,但对我来说,感觉就像是一扇破窗。
RubberDuck

2
@RubberDuck:添加一个完整的观察者,并以发布者/订阅者的机制“以防万一”,在我真的不需要的时候,感觉像是过度设计-这并不更好。
布朗

1
我不同意它可能会超出工程学。我可能会感觉到自己的方式,因为在我选择的堆栈中实现它很简单。无论如何,我们只是同意不同意?
RubberDuck
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.