挂钩何时才是正确的设计选择?


10

我曾经在一个大型的Rails应用程序上工作,在该应用程序中ActiveRecord回调的使用非常猖ramp且令人痛苦。保存记录通常会带来意想不到的副作用,这是对系统进行推理的挑战。

同时,我已经看到钩子作为继承的一部分发挥了很好的作用(例如,使用模板方法的父类允许子类添加专门的行为,而无需了解父内部的内部)。 (例如,在emacs模式被激活时运行钩子,允许用户围绕该模式添加自定义行为)。

我意识到Rails应用程序和Lisp解释器是完全不同的系统,但是我很好奇,当人们决定挂钩是否是解决他们所面临的问题的正确设计选择时,人们是否会参考任何众所周知的标准。

让我大吃一惊的主题是可预测性。钩子的滥用似乎会导致远距离的怪异动作和令人惊讶的行为,而正确使用钩子可能会导致可预测的框架而无需紧密耦合。

由于我还只有几年的编程生涯,因此我在很多方面都认为自己是菜鸟,并且怀疑人们对此主题已经投入了很多思考。什么准则可以指导这一决定?

Answers:


5

当您想通过将控制权转移给钩子的接收者来使抽象的实现细节与其使用者分离时,钩子是一个不错的设计选择。

您可以通过匿名广播(例如事件或匿名回调)或使用类型化的抽象(例如接口或父类)来实现。

在以下情况下,您应该使用匿名广播:

  • 挂钩的调用是可选的
  • 呼叫者不在乎是谁钩住了钩子。
  • 接收器的执行顺序无关紧要。
  • 您想向钩子的所有订阅者广播对象状态。

在以下情况下,应使用类型化抽象:

  • 调用该挂钩是必须的。
  • 调用对象需要标识挂钩的接收者。
  • 接收器的执行顺序与您的对象有关。

广播钩子的一个示例是KeyPressedEvent。触发事件的类并不关心谁接收事件,而是将键盘状态广播给订阅该事件的任何人。接收器的执行顺序对触发事件的类的对象状态没有影响。

您提到的模板方法是类型化抽象挂钩的一个示例。在这种情况下,父类需要为其模板方法实现,它知道接收者将是其子对象,并且它对模板方法具有特定的执行顺序(由父类定义)。


7

大多数语言和大多数平台都使用钩子,即使将其打扮成其他东西也是如此。每当您听到术语“事件”,“消息”,“触发”,“信号”或其他类似性质的术语时,您可能都在处理钩子,尽管该规则有例外,但可能不会讨论的重点。

您使用它们是因为平台为您提供了它们,或者出于性能原因甚至可能需要使用它们。使用挂钩通常可以降低代码复杂性,有助于满足业务需求,并减少CPU使用率,从而延长电池和硬件寿命。只要您希望从代码中获得最佳性能,就应该使用它们。

即使您在Rails上的痛苦经历也几乎可以确定钩子的一个常见用例:必须强制执行业务规则,并且钩子几乎可以普遍用于执行业务规则。不幸的是,并非所有规则都有意义,而且正如您所知,这对于开发人员来说是任意的,这会使事情变得更难,但这就是为什么系统文档与代码本身同样重要。

通常,使用钩子是因为它们是性能功能,并且将使您的代码比替代方法(称为“轮询”)的运行效率更高(在这种情况下,您在繁忙的循环中等待检查​​事件)。但是,还请确保使用适当的文档并保持最新。钩子几乎用在您会遇到的每种语言中,了解它们为什么存在很重要。


1

我以前曾从事过一个程序,我认为该程序很好地使用了钩子。在我的书中,真正的钩子是对回调(订阅者)列表的调用(发布)。它功能强大,滥用很容易。询问用例的最佳问题是:我想在运行时启用还是禁用行为?例如,您可能希望允许您的用户提供将在某些事件上运行的脚本,或者制作由较小插件组成的通用程序。然后挂钩调用是适当的。通常,您别无选择。

如果没有,那么您应该摆脱一个良好的模块拱门。模板方法可以很好地与重载函数配合使用,因此您不需要运行时逻辑。考虑到它将强制执行松散耦合是一种错觉:您直接调用或钩住的对象之间具有相同的依赖级别,无论如何,被调用方都必须履行其合同。

请注意,也许有人用重载函数挂钩来调用模板方法,但这与上面的方法有所不同:这里您有一个静态链接,因此陷入维护噩梦的风险较低。

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.