依赖倒置、控制反转和依赖注入辨析
?
在《道法自然——面向对象实践指南》一书中,我们采用了一个对立统一的辩证关系来说明“模板方法”模式—— “正向依赖 vs. 依赖倒置”(参见:《道法自然》第15章[王咏武, 王咏刚 2004])。这种把“好莱坞”原则和 “依赖倒置”原则等量齐观的看法其实来自于轻量级容器PicoContainer主页上的一段话:
“控制反转(Inversion of Control)的一个著名的同义原则是由Robert C. Martin提出的依赖倒置原则(Dependency Inversion Principle),它的另一个昵称是好莱坞原则(Hollywood Principle:不要调用我,让我来调用你)”[PicoContainer 2004]。
和网友们在CSDN Blog上进行了深入的讨论后,我又把这些概念重新梳理了一下。我发现,这几个概念虽然在思路和动机等宏观层面上是统一的,但在具体的应用层面还是存在着许多很微妙的差别。本文通过几个简单的例子对依赖倒置(Dependency Inversion Principle)、控制反转(Inversion of Control)、依赖注入(Dependency Injection)等概念进行了更为深入的辨析,也算是对于《道法自然》正文内容的一个补充吧。
依赖和耦合(Dependency and Coupling)?
Rational Rose的帮助文档上是这样定义“依赖”关系的:“依赖描述了两个模型元素之间的关系,如果被依赖的模型元素发生变化就会影响到另一个模型元素。典型的,在类图上,依赖关系表明客户类的操作会调用服务器类的操作。”
Martin Fowler在《Reducing Coupling》一文中这样描述耦合:“如果改变程序的一个模块要求另一个模块同时发生变化,就认为这两个模块发生了耦合。” [Fowler 2001]
从上面的定义可以看出:如果模块A调用模块B提供的方法,或访问模块B中的某些数据成员(当然,在面向对象开发中一般不提倡这样做),我们就认为模块A依赖于模块B,模块A和模块B之间发生了耦合。
那么,依赖对于我们来说究竟是好事还是坏事呢?
由于人类的理解力有限,大多数人难以理解和把握过于复杂的系统。把软件系统划分成多个模块,可以有效控制模块的复杂度,使每个模块都易于理解和维护。但在这种情况下,模块之间就必须以某种方式交换信息,也就是必然要发生某种耦合关系。如果某个模块和其它模块没有任何关联(哪怕只是潜在的或隐含的依赖关系),我们就几乎可以断定,该模块不属于此软件系统,应该从系统中剔除。如果所有模块之间都没有任何耦合关系,其结果必然是:整个软件不过是多个互不相干的系统的简单堆积,对每个系统而言,所有功能还是要在一个模块中实现,这等于没有做任何模块的分解。
因此,模块之间必定会有这样或那样的依赖关系,永远不要幻想消除所有依赖。但是,过强的耦合关系(如一个模块的变化会造成一个或多个其他模块也同时发生变化的依赖关系)会对软件系统的质量造成很大的危害。特别是当需求发生变化时,代码的维护成本将非常高。所以,我们必须想尽办法来控制和消解不必要的耦合,特别是那种会导致其它模块发生不可控变化的依赖关系。依赖倒置、控制反转、依赖注入等原则就是人们在和依赖关系进行艰苦卓绝的斗争过程中不断产生和发展起来的。
接口和实现分离?
把接口和实现分开是人们试图控制依赖关系的第一个尝试,图 1是Robert C. Martin在《依赖倒置》[Martin 1996]一文中所举的第一个例子。其中,ReadKeyboard()和WritePrinter()为函数库中的两个函数,应用程序循环调用这两个函数,以便把用户键入的字符拷贝到打印机输出。
依赖倒置(Dependency Inversion Principle)?
可以看出,上面讨论的简单分离接口的方法对于依赖关系的消解作用非常有限。Java语言提供了纯粹的接口类,这种接口类不包括任何实现代码,可以更好地隔离两个模块。C++语言中虽然没有定义这种纯粹的接口类,但所有成员函数都是纯虚函数的抽象类也不包含任何实现代码,可以起到类似于Java接口类的作用。为了和上一节中提到的简单接口相区别,本文后面将把基于Java 接口类或C++抽象类定义的接口称为抽象接口。依赖倒置原则就是建立在抽象接口的基础上的。Robert Martin这样描述依赖倒置原则[Martin 1996]:
A. 上层模块不应该依赖于下层模块,它们共同依赖于一个抽象。
B. 抽象不能依赖于具象,具象依赖于抽象。
其含义是:为了消解两个模块间的依赖关系,应该在两个模块之间定义一个抽象接口,上层模块调用抽象接口定义的函数,下层模块实现该接口。如图 3所示,对于上一节的例子,我们可以定义两个抽象类Reader和Writer作为抽象接口,其中的Read()和Write()函数都是纯虚函数,而具体的KeyboardReader和PrinterWriter类实现了这些接口。当应用程序调用Read()和Write()函数时,由于多态性机制的作用,实际调用的是具体的KeyboardReader和PrinterWriter类中的实现。因此,抽象接口隔离了应用程序和类库中的具体类,使它们之间没有直接的耦合关系,可以独立地扩展或重用。例如,我们可以用类似的方法实现FileReader或DiskWriter类,应用程序既可以根据需要选择从键盘或文件输入,也可以选择向打印机或磁盘输出,甚至同时完成多种不同的输入、输出任务。由此可以总结出,这种通过抽象接口消解应用程序和类库之间依赖关系的做法具有以下特点:
1. 应用程序调用类库的抽象接口,依赖于类库的抽象接口;具体的实现类派生自类库的抽象接口,也依赖于类库的抽象接口。
2. 应用程序和具体的类库实现完全独立,相互之间没有直接的依赖关系,只要保持接口类的稳定,应用程序和类库的具体实现都可以独立地发生变化。
3. 类库完全可以独立重用,应用程序可以和任何一个实现了相同抽象接口的类库协同工作。
控制反转(Inversion of Control)?
前面描述的是应用程序和类库之间的依赖关系。如果我们开发的不是类库,而是框架系统,依赖关系就会更强烈一点。那么,该如何消解框架和应用程序之间的依赖关系呢?
《道法自然》第5章描述了框架和类库之间的区别:
“框架和类库最重要的区别是:框架是一个‘半成品’的应用程序,而类库只包含一系列可被应用程序调用的类。
“类库给用户提供了一系列可复用的类,这些类的设计都符合面向对象原则和模式。用户使用时,可以创建这些类的实例,或从这些类中继承出新的派生类,然后调用类中相应的功能。在这一过程中,类库总是被动地响应用户的调用请求。
“框架则会为某一特定目的实现一个基本的、可执行的架构。框架中已经包含了应用程序从启动到运行的主要流程,流程中那些无法预先确定的步骤留给用户来实现。程序运行时,框架系统自动调用用户实现的功能组件。这时,框架系统的行为是主动的。
“我们可以说,类库是死的,而框架是活的。应用程序通过调用类库来完成特定的功能,而框架则通过调用应用程序来实现整个操作流程。框架是控制倒置原则的完美体现。”
框架系统的一个最好的例子就是图形用户界面(GUI)系统。一个简单的,使用面向过程的设计方法开发的GUI系统如图 5所示。
依赖注入(Dependency Injection)?
在前面的例子里,我们通过“依赖倒置”原则,最大限度地减弱了应用程序Copy类和类库提供的服务Read,Write之间的依赖关系。但是,如果需要把Copy()函数也实现在类库中,又会发生什么情况呢?假设在类库中实现一个“服务类”,“服务类”提供Copy()方法供应用程序使用。应用程序使用时,首先创建“服务类”的实例,调用其中的Copy()函数。“服务类”的实例初始化时会创建KeyboardReader 和PrinterWriter类的实例对象。如图 8所示。
结论?
分离接口和实现是人们有效地控制依赖关系的最初尝试,而纯粹的抽象接口更好地隔离了相互依赖的两个模块,“依赖倒置”和 “控制反转”原则从不同的角度描述了利用抽象接口消解耦合的动机,GoF的设计模式正是这一动机的完美体现。具体类的创建过程是另一种常见的依赖关系,“依赖注入”模式可以把具体类的创建过程集中到合适的位置,这一动机和GoF的创建型模式有相似之处。
这些原则对我们的实践有很好的指导作用,但它们不是圣经,在不同的场合可能会有不同的变化,我们应该在开发过程中根据需求变化的可能性灵活运用。
参考文献?
[PicoContainer 2004] http://www.picocontainer.org/Inversion+of+Control
[Martin 1996] The Dependency Inversion Principle, Robert C. Martin, C++ Report, May, 1996, http://www.objectmentor.com/resources/articles/dip.pdf
[Fowler 2001] Martin Fowler: Reducing Coupling. IEEE Software 18(4): 102-104 (2001) http://www.martinfowler.com/ieeeSoftware/coupling.pdf
[Folwer 2004] Inversion of Control Containers and the Dependency Injection pattern http://martinfowler.com/articles/injection.html
[透明2004] 透明,Inversion of Control Containers and the Dependency Injection pattern 的译文http://gigix.blogdriver.com/gigix/inc/DependencyInjection.pdf
[王咏武, 王咏刚 2004] 王咏武, 王咏刚. 道法自然—面向对象实践指南. 电子工业出版社, 2004