控制反转

2005年6月26日

控制反转是你在扩展框架时经常遇到的常见现象。事实上,它通常被视为框架的定义特征之一。

让我们考虑一个简单的例子。假设我正在编写一个从用户获取信息的程序,并且我使用命令行查询。我可能会这样做

  #ruby
  puts 'What is your name?'
  name = gets
  process_name(name)
  puts 'What is your quest?'
  quest = gets
  process_quest(quest)

在这个交互中,我的代码处于控制之中:它决定何时提出问题,何时读取响应以及何时处理这些结果。

但是,如果我使用窗口系统来执行类似的操作,我将通过配置窗口来完成。

  require 'tk'
  root = TkRoot.new()
  name_label = TkLabel.new() {text "What is Your Name?"}
  name_label.pack
  name = TkEntry.new(root).pack
  name.bind("FocusOut") {process_name(name)}
  quest_label = TkLabel.new() {text "What is Your Quest?"}
  quest_label.pack
  quest = TkEntry.new(root).pack
  quest.bind("FocusOut") {process_quest(quest)}
  Tk.mainloop()

现在,这些程序之间的控制流存在很大差异,特别是process_nameprocess_quest方法的调用时间。在命令行形式中,我控制何时调用这些方法,但在窗口示例中,我不控制。相反,我将控制权交给窗口系统(使用Tk.mainloop命令)。然后,它根据我在创建表单时进行的绑定来决定何时调用我的方法。控制被反转 - 它调用我,而不是我调用框架。这种现象就是控制反转(也称为好莱坞原则 - “不要打电话给我们,我们会打电话给你”)。

框架的一个重要特征是,用户定义的用于定制框架的方法通常会在框架本身内部调用,而不是从用户的应用程序代码调用。框架通常扮演协调和排序应用程序活动的主程序的角色。这种控制反转赋予框架作为可扩展骨架的能力。用户提供的方法根据特定应用程序定制框架中定义的通用算法。

-- Ralph Johnson 和 Brian Foote

控制反转是使框架与库不同的关键部分。库本质上是一组你可以调用的函数,如今通常组织成类。每次调用都会执行一些工作,并将控制权返回给客户端。

框架体现了一些抽象设计,并且内置了更多行为。为了使用它,你需要通过子类化或插入你自己的类来将你的行为插入框架中的各个位置。然后,框架的代码在这些点调用你的代码。

你可以通过多种方式将你的代码插入以供调用。在上面的 ruby 示例中,我们在文本输入字段上调用一个 bind 方法,该方法将事件名称和一个Lambda作为参数传递。每当文本输入框检测到事件时,它都会调用闭包中的代码。使用这样的闭包非常方便,但许多语言不支持它们。

另一种方法是让框架定义事件,并让客户端代码订阅这些事件。.NET 是一个很好的例子,它具有允许人们在小部件上声明事件的语言特性。然后,你可以使用委托将方法绑定到事件。

以上方法(它们实际上是相同的)适用于单个情况,但有时你希望将多个必需的方法调用组合成一个扩展单元。在这种情况下,框架可以定义一个接口,客户端代码必须实现该接口以进行相关调用。

EJB 是这种控制反转风格的一个很好的例子。当你开发一个会话 Bean 时,你可以实现各种方法,这些方法会在各种生命周期点被 EJB 容器调用。例如,Session Bean 接口定义了ejbRemoveejbPassivate(存储到辅助存储器)和ejbActivate(从被动状态恢复)。你无法控制何时调用这些方法,只能控制它们做什么。容器调用我们,我们不调用它。

这些是控制反转的复杂情况,但你在更简单的场景中也会遇到这种效果。一个模板方法就是一个很好的例子:超类定义了控制流,子类扩展了这种覆盖方法或实现抽象方法来进行扩展。因此,在 JUnit 中,框架代码会为你调用setUptearDown方法,以创建和清理你的测试夹具。它进行调用,你的代码做出反应 - 所以控制再次被反转。

如今,由于 IoC 容器的兴起,人们对控制反转的含义存在一些混淆;有些人将这里的一般原则与这些容器使用的控制反转的特定风格(例如依赖注入)混淆。这个名字有点令人困惑(而且具有讽刺意味),因为 IoC 容器通常被认为是 EJB 的竞争对手,但 EJB 使用控制反转的程度一样多(如果不是更多的话)。

词源:据我所知,控制反转一词最早出现在 Johnson 和 Foote 在 1988 年发表在《面向对象编程杂志》上的论文设计可重用类中。这篇论文是那些经久不衰的论文之一 - 在十五年后的今天,它仍然值得一读。他们认为这个词是从其他地方来的,但记不起来是什么。然后,这个词渗透到面向对象社区,并出现在四人帮的书中。更生动的同义词“好莱坞原则”似乎起源于 Richard Sweet 在 1983 年关于 Mesa 的一篇论文。在一份设计目标列表中,他写道:“不要打电话给我们,我们会打电话给你(好莱坞法则):工具应该安排 Tajo 在用户希望向工具传达某些事件时通知它,而不是采用“向用户询问命令并执行它”的模型。”John Vlissides 为 C++ 报告写了一篇专栏,它很好地解释了“好莱坞原则”下的概念。(感谢 Brian Foote 和 Ralph Johnson 帮助我了解词源。)