控制反转是扩展框架时常见的一种现象。实际上,它经常被视为框架的一个定义性特征。
让我们考虑一个简单的例子。想象一下,我正在编写一个程序,从用户那里获取一些信息,并且我正在使用命令行询问。我可以这样做
#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_name
和process_quest
方法时的控制。在命令行表单中,由我控制何时调用这些方法,但是在窗口示例中,我不这样做。相反,我将控制权交给窗口系统(使用Tk.mainloop
命令)。 然后根据我在创建表单时所做的绑定,决定何时调用我的方法。 控件是反向的——它调用我而不是调用框架。这种现象就是控制反转(也被称为好莱坞原则——“别给我们打电话,我们会给你打电话的”)。
框架的一个重要特征是,由用户定义的用于定制框架的方法常常从框架内部调用,而不是从用户的应用程序代码调用。框架通常在协调和排序应用程序活动方面扮演主程序的角色。这种控制反转赋予框架作为可扩展框架的能力。用户提供的方法为特定应用程序定制框架中定义的通用算法。
控制反转是使框架不同于库的关键部分。库本质上是一组可以调用的函数,现在通常被组织成类。每个调用都会执行一些操作,并将控制权返回给客户端。
框架体现了一些抽象的设计,内置了更多行为。为了使用它,你需要通过子类化或插入你自己的类来将你的行为插入框架中的不同位置。然后框架的代码在这些点上调用你的代码。
你可以通过多种方式插入要调用的代码。 在上面的ruby示例中,我们在文本输入字段上调用bind方法,该方法将事件名称和Lambda作为参数传递。 只要文本输入框检测到事件,它就会调用闭包中的代码。 使用这样的闭包非常方便,但许多语言不支持它们。
另一种方法是让框架定义事件,并让客户端代码订阅这些事件。 .NET就是一个很好的例子,它提供了语言特性,允许人们在小部件上声明事件。 然后,你可以使用委托将方法绑定到事件。
上述方法(它们实际上是相同的)适用于单个案例,但有时你希望在单个扩展单元中组合几个必需的方法调用。 在这种情况下,框架可以定义一个接口,为了相关调用,客户端代码必须实现的这个接口。
EJB是这种控制反转方式的一个很好的例子。当你开发会话bean时,你可以实现EJB容器在不同生命周期点调用的各种方法。例如,Session Bean接口定义ejbRemove
,ejbPassivate
(存储到二级存储)和ejbActivate
(从被动状态恢复)。你不能控制什么时候调用这些方法,只能控制它们做什么。容器调用我们,我们不调用它。
这些是控制反转的复杂情况,但是在更简单的情况下,也能达到这个效果。 模板方法就是一个很好的例子:超类定义了控制流,子类扩展了这个重写方法或实现抽象方法来进行扩展。所以,在JUnit中,框架代码调用setUp
和tearDown
方法来创建和清理文本夹具。 它执行调用,你的代码作出反应 - 所以再次控制被反转。
由于IoC容器的兴起,最近对控制反转的含义存在一些混淆;有些人将这里的一般原则与这些容器使用的控制反转的特定样式(例如依赖注入)相混淆。这个名称有点令人困惑(而且具有讽刺意味)因为IoC容器通常被认为是EJB的竞争对手,但是EJB也使用了同样多的控制反转(如果不是更多的话)。