Xiao Peng

My personal blog moves to xiaopeng.me , blogs about design patterns will be synced to here.
肖鹏,ThoughtWorks资深咨询师,目前关注于架构模式、敏捷软件开发等领域,并致力于软件开发最佳实践的推广和应用。
多次为国内大型企业敏捷组织转型提供咨询和培训服务,在大型团队持续集成方面具有丰富的经验。
  博客园  :: 首页  :: 联系 :: 订阅 订阅  :: 管理

按理说这三个词都不算新鲜了。本文主要是整理一下其他人对这几个概念的论述。

本文要回答的基本问题:IoC(和其他几个技术)到底解决什么问题?

历史

有很多术语名词,由于时代的变迁,其含义可能发生了变化或者人们忘记了当初引入这个名词或者技术的背景。我们简单回顾一些IoC及其相关术语和技术的背景,希望从中能够找到一些理解这些技术的线索。

目前可以在网上找到的最早的资料是1988年的Design Reusable Classes[1]。

One of the most important kinds of reuse is reuse of designs.

An object-oriented abstract design, also called a framework, consists of an abstract class for each major component.

Frameworks provide a way of reusing code that is resistant to more conventional reuse attempts.

A framework's application specific behavior is usually defined by adding methods to subclasses of one or more of its classes. Each method added to a subclass must abide by the internal conventions of its superclasses. We call these white-box frameworks because their implementation must be understood to use them.

The major problem with such a framework is that every application requires the creation of many new subclasses.

A second problem is that a white-box framework can be difficult to learn to use, since learning to use it is the same as learning how it is constructed.

Another way to customize a framework is to supply it with a set of components that provide the application specific behavior. Each of these components will be required to understand a particular protocol. All or most of the components might be provided by a component library. The interface between components can be defined by protocol, so the user needs to understand only the external interface of the components. Thus, this kind of a framework is called a black-box framework.

There is a set of black-box components of MVC called the pluggable views.

A framework becomes more reusable as the relationship between its parts is defined in terms of a protocol, instead of using inheritance. In fact, as the design of a system becomes better understood, black-box relationships should replace white-box ones. Black-box relationships are an ideal towards which a system should evolve.

这里给出了控制反转的本意——方便抽象设计的重用。

其他的重要事件包括:

  • 1983年Richard E.Sweet提出“好莱坞原则”。
  • 1988年Ralph E.Johson引用“Inversion of Control”。
  • 1994年GoF在Design Patterns中推广“好莱坞原则”。
  • 1995年Robert C. Martin提出“依赖倒置原则(DIP)”。
  • 1998年IoC作为术语得到推广。Steffano
  • 2003年Spring、Pico等框架出现
  • 2004年MF的Dependency Injection论文。

IoC-timeline

来自:http://picocontainer.org/inversion-of-control-history.html

控制反转

我们先举个例子看现实世界和软件世界是如何对应的。我们举出版社给编辑分配任务的例子:

话说有一家出版社,作者提交了自己的手稿,编辑部主任就会根据其领域分配给相应的编辑。没有计算机的时候他们是这样工作的。编辑部主任那里有一摞空白的任务分配书。收到作者提交的手稿,主任就会看一下手稿所在的领域,然后对照一下编辑列表,找到负责这个领域的编辑。编辑部主任的任务分配书是这样的:

任务分配书

编号:20110229

手稿名称:面向模式的软件架构(第五卷)

译者:肖鹏

责任编辑:李剑

编辑部主任(签字盖章):郭晓

这样的任务分配书每天郭晓都要签发很多份。

接下来,我们要用软件系统来实现了。

首先是“任务分配书”。在我们的代码里面,要不要对每一份任务分配书进行建模呢?显然不需要。我们只要建一个类就可以了——TaskAssignment。这个类有一些属性,或者是字符串或者是数字或者是更复杂的对象。是的这个类会产生很多实例,但是在我们的代码里面却只有一份。注意这是一个非常重要的区别,下面我们还会回到这一点上来。

任务分配书是一个“事物”,我们除了要对这个事物建模之外还要对产生这个事物的“过程(或者方式)”进行建模。在现实世界里面,这个过程是这样的——收到手稿->分析所在领域->查找合适编辑->签字->交给相应编辑。在软件世界里面可能是这样的——收到提醒->分析所在领域->查找合适编辑->点击“分配”按钮。在代码世界里面对应的是——一条消息、一个属性、一个列表、一个按钮、按钮背后绑定的逻辑(比如记个日志、发个邮件等等)。

由此,我们可以看出,至少对于一类问题,我们可以抽象为——事物+过程。而现代计算机技术特别是编程技术的发展,分别为我们对这两个方面的建模提供了很多辅助手段。比如,在TaskAssignment类中的字符串我们是不用自己写的;而这种典型的工作流也有很多现成的引擎可供使用。

特别的,我们把第二种辅助手段称为——控制反转。

首先控制反转这个词实际上不太合适,因为基本上算不上是反转,相对来说依赖倒置倒是有道理。为什么这么说呢?我们先来看一下MF给的控制反转的例子:

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() 

注意加灰的两行代码。我们把处理名字和处理请求绑定到失去焦点事件上,但是什么时候失去焦点,并不是我们的代码决定的。 MF说,因为“是框架调用我的代码,而不是我调用框架的代码”就“反转”了。这样多少有点讲不通,因为在第一段代码里面没有“框架”这个东西。所以有人建议[wiki]改名叫“Handing over of Control”——只不过将控制交出去而已,无所谓反转不翻转。

结论:

IoC没有解决什么特别的问题,IoC就是有些事情我们本来要做的,别人已经做了,我们教给别人去做。特别的,只有将流程的控制教给别人去做的时候才叫做IoC,而将实现交给别人去做不叫IoC。

IoC容器

当我们要把一个流程交给别人去做的时候,那个别人往往不是特定为我们实现的。所以,这里面必然有一些共性的东西可以抽出来的。我们在长期的开发过程中发现了这样一些共性的东西。

  1. 一个对象要使用必须先创建或者说实例化。(这个没有容器你也可以做的)
  2. 我们的对象经常依赖别的对象,我们需要把这些依赖装配给我们的对象。(这个没有容器你也可以做的)
  3. 我们经常要对一些对象做配置。其实就是配置注入,可以理解为依赖注入的一种特殊情况。(这个没有容器你也可以做的)

为什么要说三遍“这个没有容器你也可以做的”呢?因为这是IoC中被反转的部分。要不要IoC容器,考虑这么几个方面:

  1. 这三个问题是不是很容易抽象?这是有没有可能的问题。
  2. 这三个问题你自己做的话你烦不烦?这是有没有必要的问题。
  3. 你交给别人去做了会不会把系统搞得更复杂或者把自己惯坏?这是个后果的问题。

我们看一下下面两个构造函数,这是一个使用Spring做IoC容器的项目。构造一个对象需要依赖这么多的对象,交给Spring去做简直是天经地义。(Spring也可以直接对field做注入,这里采用构造函数注入主要是为了测试方便。)我们知道构造函数参数列表过长是个坏味道,可是当我们使用IoC容器的时候,我们会慢慢地提高自己的容忍度,因为那个神秘的XML文件和那个强大的框架可以帮我们轻易地构造出不论多么复杂的对象。

@Autowired
public AssignmentService(ManuscriptRoleDAO msrDAO,
    UserTaskEmailBuildService userTaskEmailBuildService,
    SendEmailBRO sendEmailBRO,
    CompleteTaskAction completeTaskAction,
    AssignmentNotificationAction assignmentNotificationAction,
     RecordEventAction recordEventAction) 
@Autowired
public DeclineTaskHandler(ManuscriptUserTaskService manuscriptUserTaskService,
    UserTaskEmailBuildService userTaskEmailBuildService, 
    SendEmailBRO sendEmailBRO,
    ManuscriptTaskDAO manuscriptTaskDAO, 
    UserTaskDAO userTaskDAO,
    ManuscriptRoleDAO manuscriptRoleDAO, 
    RecordEventAction recordEventAction) 

 

我们可以看到IoC这个帽子对于Spring、PicoContainer这样的框架来说实在是有点大。MF察觉到了这一点,于是一篇非常著名的文章出现了。http://martinfowler.com/articles/injection.html

依赖注入

PicoContainer的团队对于IoC容器改名为为DI容器不太认同。他们认为DI只关注IoC容器三个方面(生命周期管理、依赖管理、配置管理)中的一个方面。我认为,生命周期管理和配置管理,实际上可以从“容器”这个词语中获得其含义。将PicoContainer称为依赖注入容器并无不妥。

依赖注入的方式有三种:构造器注入、设置方法注入和接口注入。

有状态与无状态

无状态对象是容易通过IoC容器直接获得的。有状态对象通常有两个地方区别于无状态对象:

  1. 它是由运行时数据配置(或装配)的;
  2. 它的方法执行可能是有先后顺序的。

在Spring中使用prototype作用域实现。

依赖倒置

依赖倒置是由Robert Martin在1995~1996年提出的。虽然在IoC容器中大多都使用了DIP原则,但是它与IoC容器(或者称为DI容器)之间并无必然联系。除非指定使用接口注入的方式,我们总是可以

语录:

MF:控制反转是框架所共有的特征。

MF:依赖注入和服务定位器(Service Locator)的差异并不太重要,重要的是两者的目标都是——将组件的配置与使用分离开。

MF:EJB也是IoC实现。

MF:对于这些容器来说,它们反转的是“如何定位插件的具体实现”。……我们决定将这个模式叫做“依赖注入”。

MF:要消除应用对插件实现的依赖,依赖注入和服务定位器可以起到同样的效果。

MF:依赖注入主要有三种形式:构造器注入、设置方法注入、接口注入。

MF:控制反转是框架的共同特征,……它会增加理解的难度……总的来说,除非必要,否则我会尽量避免使用它。很多时候……更为直观的方式(比如Service Locator)会比较合适。

MF:如果你不能轻松地用一些“伪”组件把服务架起了,以便于测试,这说明你的设计出现了严重的问题。

MF:我首选的方案是尽量在构造阶段就创建完整合法的对象。

MF:我常常发现人们经常急于定义配置文件。

RJ:我一般推荐设值方法注入。(与MF相反)

RJ:但凡容器可以解决的问题,就不要自己编写代码来解决。(与MF相反)

RJ:IoC实现的策略有两种:依赖查找和依赖注入。

 

参考文献:

Richard E.Johson: 好莱坞原则(1983),http://www.digibarn.com/friends/curbow/star/XDEPaper.pdf

Ralph E.Jonson: Design Reusable Class(1988),http://www.laputan.org/drc/drc.html

熊节:http://www.docin.com/p-6038945.html(译自MF的文章)

郑晔:

Martin Folwer: 2004,http://martinfowler.com/articles/injection.html

Martin Fowler: 2005,http://martinfowler.com/bliki/InversionOfControl.html

PicoContainer: http://picocontainer.org/inversion-of-control.html

PicoContainer: http://picocontainer.org/inversion-of-control-history.html

Paul Hammant: Inversion of Control Rocks, http://java.sys-con.com/node/38102

Wiki: http://en.wikipedia.org/wiki/Design_Patterns

Rod johnson: J2EE Development without EJB