单个函数处理事件或每个事件的函数?

时间:2015-12-04 08:28:11

标签: swift

在哪种情况下更喜欢什么:单个函数处理事件或每个事件的函数?

这是一个基本的代码示例:

选项1

enum Notification {
    case A
    case B
    case C
}

protocol One {
    func consumer(consumer: Consumer, didReceiveNotification notification: Notification)
}

选项2

protocol Two {
    func consumerDidReceiveA(consumer: Consumer)
    func consumerDidReceiveB(consumer: Consumer)
    func consumerDidReceiveC(consumer: Consumer)
}

背景

Apple使用这两个选项。例如。对于NSStreamDelegate,我们有第一个选项,而在CoreBluetooth(例如CBCentralManagerDelegate)中,我们会看到选项二。

我看到的一个很大的区别是,Swift不能很好地支持可选的协议方法(通过extension@obj keyword)。

你更喜欢什么?什么是(dis)优势?

1 个答案:

答案 0 :(得分:3)

在实现最宽松的耦合形式和最高程度的凝聚力方面,自然选择会影响个别事件,而不是这种多事件责任束。

然而,有很多实际问题可能会让你倾向于采用相反的,更粗略的方式来处理事件而不是每个粒子事件的单独函数。

以下是一些可能的(未按任何特定顺序列出)。

<强>样板

虽然这不是最令人担心的事情,但编写一堆函数往往比在一个函数中编写一堆if/else语句或switch个案例要花费更多的精力。然而,更重要的是将事件处理槽连接/断开事件处理信号所需的代码。避免为每个处理的小事件编写订阅/取消订阅类型代码的需要可以大大节省要维护的代码量。

<强>性能

表现可能有利于更粗糙的多事件处理程序,这似乎是违反直觉的。毕竟,粒度事件处理程序需要较少的分支(一个动态分派到达精确的事件处理程序),而较粗的一个需要两倍(一个动态分派到达粗略的事件处理站点,另一个本地系列)分支以获得精确的事件处理代码。)

然而,动态调度的成本在很大程度上依赖于分支预测。如果您要分支到更粗略的事件处理程序,那么通常您会更频繁地分支到同一组指令中,这可能是一种优化策略。拥有两组更可预测的分支通常可以产生比一个不太可预测的分支更优的结果。

此外,较粗略的事件处理通常意味着更少的聚合,更少的函数列表在这些触发事件的一侧调用。这可以转化为减少的内存使用量和改进的参考局部性。

另一方面,分支到更粗略的事件处理程序通常意味着更频繁地分支。例如,某些网站可能只对push种输入事件感兴趣,而不是resize事件。如果我们将所有这些整合到一个粗略的事件处理程序中并且顶部没有一些过滤机制,那么通常我们将不得不支付动态调度的成本,即使对于resize事件来说也是如此。特定网站。

然而,我发现这实际上往往比我想象的更好地将不必要地分支到相同的粗略函数(很可能是由于分支预测器成功)而不是分支到各种各样的不同功能,只在需要时。

所以这里有一个平衡的行为,甚至表现并没有明确地反对另一个策略。它仍然根据具体情况而变化。

然而,由于缺乏关于关键代码路径的测量和非常详细的数据,从性能角度来看,它通常比这些更粗糙的多事件处理程序更容易出错。毕竟,即使从性能角度证明这是错误的决定,从粗到细也更容易优化(我们甚至可以通过保持粗略和使用细粒度事件来非常非侵入性地进行 - 处理最有益于它的情况),反之亦然。

活动订阅/取消订阅

这同样可以以某种方式摆动,但根据我的经验(来自团队设置),与事件处理相关的大多数人为错误不会发生在事件处理代码中,而是发生在外部。我看到的最常见的错误来源是未能订阅事件,而且最常见的是,当事件不再令人感兴趣时,未能取消订阅。

当事件在较粗糙的级别处理时,通常较少涉及容易出错的订阅/取消订阅代码(这与上面的样板问题有关,但这是一种不寻常的样板,因为它可以是非常容易出错而且写作不仅乏味。

这也是非常具体的情况。在我经常参与的系统中,通常需要实体继续存在,过早地取消订阅某些事件。那些过早的案例通常要求代码取消订阅要手动编写的事件,因为它们不能与实体的生命周期联系起来。这可能更多地指向其他地方的设计问题,但在这种情况下,团队范围内的错误数量随着更粗略的事件处理而下降。

类型安全

虽然此处的示例中未显示,但通常使用较粗略的事件处理需要通过更通用的参数来挤压更多不同类型的数据。这可能会在C等极端情况下转换为通过void指针和更危险的指针类型转换来压缩更多数据。这样,编译时类型的安全性就会消失,我们可以开始看到一个全新的人为错误来源。

在更高级别的语言中,当我们无法对委托的签名进行建模以完全符合触发事件时传入的参数时,这可能会转化为更多的演员表或类似的东西。

我通常发现这不是混淆和错误的最大来源,前提是在投射或取消装箱这些参数时至少存在某种形式的运行时类型安全性。但它更倾向于事件处理。

智力开销

这可能因人而异,但我倾向于从非常管理/概述的角度来看待系统,特别是在控制流方面。这是因为我倾向于在系统的较低级别部分工作,包括专有的UI工具包等。

在这种情况下,按下按钮时,会调用哪些功能?它在由数十万个小函数组成的大规模代码库中变成了一个谜,无需跟踪按下按钮时实际调用的代码并查看被调用的每个函数。

这是一个事件驱动范式的必然性和我从未变得百分之百舒服的东西,但我发现它减轻了我在个人心理模型中看到的一些爆炸性复杂性(类似于非常复杂的东西)图表)当代码分散程度较低时。使用较粗糙的事件处理程序可以在这样的按钮推送中将更少,更集中的功能分支到整个系统中,这有助于我在心理图中涉及的功能较少但功能较大时提高我的熟悉度。

这里有一个非常简单的实际好处,如果你想知道特定实体何时响应一系列事件,我们可以简单地在这个粗略的事件处理站点上放置一个断点(同时仍然能够钻取通过在本地代码分支中放置断点来关闭该特定实体的特定事件。

当然,我可能会在每个人都使用的低级系统中工作。似乎很多人都对在代码中订阅按钮推送事件的想法感到满意,而不必担心同一事件的所有其他订阅者。

从我对系统的整体控制流程图来看,它有助于我在代码库中有更少但更粗糙的事件处理站点时更容易地吸收复杂性,即使我通常发现单片函数是一种负担。特别是在调试环境中,我面临一个问题,例如&#34;是什么导致了这种情况发生?&#34; ,结合了&#34;的事件处理问题。当发生这种情况时,实际上会调用哪些函数?&#34; 可以真正地增加复杂性。由于处理事件的潜在目标站点较少,后一个问题得到缓解。

<强>结论

因此,这些因素可能会影响您选择一种设计策略而不是另一种设计策略。我发现自己处于中间的某个地方。我通常不会在Windows上选择设计粗略,wndproc,它想要为可以想象的每个窗口事件关联单个超粗事件处理程序。然而,我可能倾向于设计更粗略的事件处理级别,而不仅仅是为了减轻这种心理复杂性,减少代码分散,可能提高性能(总是手持一个分析器)。

然后有时我选择在非常精细的层面进行设计时,复杂性不是那么好(通常当包触发事件不是那个中心)时,当性能不是很高时。 ta关注或性能实际上有利于这条路线,并提高了类型的安全性。这完全是个案。