向依赖关系宣战 依赖倒置、控制反转和依赖注入辨析.docx

上传人:牧羊曲112 文档编号:5082338 上传时间:2023-06-02 格式:DOCX 页数:15 大小:529.73KB
返回 下载 相关 举报
向依赖关系宣战 依赖倒置、控制反转和依赖注入辨析.docx_第1页
第1页 / 共15页
向依赖关系宣战 依赖倒置、控制反转和依赖注入辨析.docx_第2页
第2页 / 共15页
向依赖关系宣战 依赖倒置、控制反转和依赖注入辨析.docx_第3页
第3页 / 共15页
向依赖关系宣战 依赖倒置、控制反转和依赖注入辨析.docx_第4页
第4页 / 共15页
向依赖关系宣战 依赖倒置、控制反转和依赖注入辨析.docx_第5页
第5页 / 共15页
亲,该文档总共15页,到这儿已超出免费预览范围,如果喜欢就下载吧!
资源描述

《向依赖关系宣战 依赖倒置、控制反转和依赖注入辨析.docx》由会员分享,可在线阅读,更多相关《向依赖关系宣战 依赖倒置、控制反转和依赖注入辨析.docx(15页珍藏版)》请在三一办公上搜索。

1、向依赖关系宣战依赖倒置、控制反转和依赖注 入辨析2011年04月04日13:18来源:普索网|关键词:依赖 关系宣战倒置控制反转转载自:在道法自然一一面向对象实践指南一书中,我们采用了一个对立统一的辩证关系来 说明“模板方法”模式一一 “正向依赖vs.依赖倒置”(参见:道法自然第15章王 咏武,王咏刚2004)。这种把“好莱坞”原则和“依赖倒置”原则等量齐观的看法其实 来自于轻量级容器PicoContainer主页上的一段话:“控制反转(Inversion of Control)的一个著名的同义原则是由Robert C. Martin 提出的依赖倒置原则(Dependency Inversio

2、n Principle),它的另一个昵称是好莱坞原则 (Hollywood Principle:不要调用我,让我来调用你)”PicoContainer 2004。和网友们在CSDNBlog上进行了深入的讨论后,我又把这些概念重新梳理了一下。我发 现,这几个概念虽然在思路和动机等宏观层面上是统一的,但在具体的应用层面还是存在着 许多很微妙的差别。本文通过几个简单的例子对依赖倒置(Dependency Inversion Principle)、控制反转(Inversion of Control)、依赖注入(Dependency Injection) 等概念进行了更为深入的辨析,也算是对于道法自然正

3、文内容的一个补充吧。依赖和耦合(Dependency and Coupling )在道法自然一一面向对象实践指南一书中,我们采用了一个对立统一的辩证关系来 说明“模板方法”模式一一 “正向依赖vs.依赖倒置”(参见:道法自然第15章王 咏武,王咏刚2004)。这种把“好莱坞”原则和“依赖倒置”原则等量齐观的看法其实 来自于轻量级容器PicoContainer主页上的一段话:首先来看一下依赖和耦合的概念。Rational Rose的帮助文档上是这样定义“依赖”关系的:“依赖描述了两个模型元素 之间的关系,如果被依赖的模型元素发生变化就会影响到另一个模型元素。典型的,在类图 上,依赖关系表明客户类

4、的操作会调用服务器类的操作。”Martin Fowler在Reducing Coupling一文中这样描述耦合:“如果改变程序的一 个模块要求另一个模块同时发生变化,就认为这两个模块发生了耦合。” Fowler 2001从上面的定义可以看出:如果模块A调用模块B提供的方法,或访问模块B中的某些数 据成员(当然,在面向对象开发中一般不提倡这样做),我们就认为模块A依赖于模块B, 模块A和模块B之间发生了耦合。那么,依赖对于我们来说究竟是好事还是坏事呢?由于人类的理解力有限,大多数人难以理解和把握过于复杂的系统。把软件系统划分成 多个模块,可以有效控制模块的复杂度,使每个模块都易于理解和维护。但在

5、这种情况下, 模块之间就必须以某种方式交换信息,也就是必然要发生某种耦合关系。如果某个模块和其 它模块没有任何关联(哪怕只是潜在的或隐含的依赖关系),我们就几乎可以断定,该模块 不属于此软件系统,应该从系统中剔除。如果所有模块之间都没有任何耦合关系,其结果必 然是:整个软件不过是多个互不相干的系统的简单堆积,对每个系统而言,所有功能还是要 在一个模块中实现,这等于没有做任何模块的分解。因此,模块之间必定会有这样或那样的依赖关系,永远不要幻想消除所有依赖。但是, 过强的耦合关系(如一个模块的变化会造成一个或多个其他模块也同时发生变化的依赖关 系)会对软件系统的质量造成很大的危害。特别是当需求发生

6、变化时,代码的维护成本将非 常高。所以,我们必须想尽办法来控制和消解不必要的耦合,特别是那种会导致其它模块发 生不可控变化的依赖关系。依赖倒置、控制反转、依赖注入等原则就是人们在和依赖关系进 行艰苦卓绝的斗争过程中不断产生和发展起来的。接口和实现分离把接口和实现分开是人们试图控制依赖关系的第一个尝试,图1是Robert C. Martin在依赖倒置Martin 1996 一文中所举的第一个例子。其中,ReadKeyboard()和WritePrinter()为函数库中的两个函数,应用程序循环调用这两个函数,以便把用户键入的 字符拷贝到打印机输出。为了使应用程序不依赖于函数库的具体实现,C语言把

7、函数的定义写在了一个分离的头 文件(函数库.h)中。这种做法的好处是:虽然应用程序要调用函数库、依赖于函数库,但 是,当我们要改变函数库的实现时,只要重写函数的实现代码,应用程序无需发生变化。例 如,改变函数库.c文件,把WritePrinter ()函数重新实现成向磁盘中输出,这时只要将应 用程序和函数库重新链接,程序的功能就会发生相应的变化。上面的函数库也可以采用C+语言来实现。我们通常把这种用面向对象技术实现的,为 应用程序提供多个支持类的模块称为“类库”,如图2所示。这种通过分离接口和实现来 消解应用程序和类库之间依赖关系的做法具有以下特点:1. 应用程序调用类库,依赖于类库。2. 接

8、口和实现的分离从一定的程度上消解了这个依赖关系,具体实现可以在编译期间 发生变化。但是,这种消解方法的作用非常有限。比如说,一个系统中无法容纳多个实现, 不同的实现不能动态发生变化,用WritePrinter函数名来实现向磁盘中输出的功能也显得 非常古怪,等等。3. 类库可以单独重用。但是应用程序不能脱离类库而重用,除非提供一个实现了相同 接口的类库。依赖倒置(Dependency Inversion Principle)可以看出,上面讨论的简单分离接口的方法对于依赖关系的消解作用非常有限。Java 语言提供了纯粹的接口类,这种接口类不包括任何实现代码,可以更好地隔离两个模块C+ 语言中虽然没

9、有定义这种纯粹的接口类,但所有成员函数都是纯虚函数的抽象类也不包含任 何实现代码,可以起到类似于Java接口类的作用。为了和上一节中提到的简单接口相区别, 本文后面将把基于Java接口类或C+抽象类定义的接口称为抽象接口。依赖倒置原则就是 建立在抽象接口的基础上的。Robert Martin这样描述依赖倒置原则Martin 1996:A. 上层模块不应该依赖于下层模块,它们共同依赖于一个抽象。B. 抽象不能依赖于具象,具象依赖于抽象。其含义是:为了消解两个模块间的依赖关系,应该在两个模块之间定义一个抽象接口, 上层模块调用抽象接口定义的函数,下层模块实现该接口。如图3所示,对于上一节的例 子,

10、我们可以定义两个抽象类Reader和Writer作为抽象接口,其中的Read()和Write() 函数都是纯虚函数,而具体的KeyboardReader和PrinterWriter类实现了这些接口。当应 用程序调用Read()和Write()函数时,由于多态性机制的作用,实际调用的是具体的 KeyboardReader和PrinterWriter类中的实现。因此,抽象接口隔离了应用程序和类库中 的具体类,使它们之间没有直接的耦合关系,可以独立地扩展或重用。例如,我们可以用类 似的方法实现FileReader或DiskWriter类,应用程序既可以根据需要选择从键盘或文件输 入,也可以选择向打印

11、机或磁盘输出,甚至同时完成多种不同的输入、输出任务。由此可以 总结出,这种通过抽象接口消解应用程序和类库之间依赖关系的做法具有以下特点:1. 应用程序调用类库的抽象接口,依赖于类库的抽象接口;具体的实现类派生自类库 的抽象接口,也依赖于类库的抽象接口。2. 应用程序和具体的类库实现完全独立,相互之间没有直接的依赖关系,只要保持接 口类的稳定,应用程序和类库的具体实现都可以独立地发生变化。3. 类库完全可以独立重用,应用程序可以和任何一个实现了相同抽象接口的类库协同 工作。一般情况下,由于类库的设计者并不知道应用程序会如何使用类库,抽象接口大多由类 库设计者根据自己设想的典型使用模式总结出来,并

12、保留一定的灵活度,以提供给应用程序 的开发者使用。但还有另外一种情况。图4是Martin Fowler在Reducing Coupling一文中使用的 一个例子Fowler 2001。其中,Domain包要使用数据库包,即Domain包依赖于数据库包。 为了隔离Domain包和数据库包,可以引入一个Mapper包。如果在特定的情况下,我们希望 Domain包能够被多次重用,而Mapper包可以随时变化,那么,我们就必须防止Domain包 过分地依赖于Mapper包。这时,可以由Domain包的设计者总结出自己需要的抽象接口(如 Store),而由Mapper包的设计者来实现该抽象接口。这样一来

13、,无论是在接口层面,还是 在实现层面,依赖关系都完全颠倒过来了。图4完全倒置了的依赖关系控制反转(Inversion of Control )前面描述的是应用程序和类库之间的依赖关系。如果我们开发的不是类库,而是框架系 统,依赖关系就会更强烈一点。那么,该如何消解框架和应用程序之间的依赖关系呢?道法自然第5章描述了框架和类库之间的区别:“框架和类库最重要的区别是:框架是一个半成品的应用程序,而类库只包含一系 列可被应用程序调用的类。“类库给用户提供了一系列可复用的类,这些类的设计都符合面向对象原则和模式。用 户使用时,可以创建这些类的实例,或从这些类中继承出新的派生类,然后调用类中相应的 功能

14、。在这一过程中,类库总是被动地响应用户的调用请求。“框架则会为某一特定目的实现一个基本的、可执行的架构。框架中已经包含了应用程 序从启动到运行的主要流程,流程中那些无法预先确定的步骤留给用户来实现。程序运行时, 框架系统自动调用用户实现的功能组件。这时,框架系统的行为是主动的。“我们可以说,类库是死的,而框架是活的。应用程序通过调用类库来完成特定的功能, 而框架则通过调用应用程序来实现整个操作流程。框架是控制倒置原则的完美体现。”框架系统的一个最好的例子就是图形用户界面(GUI)系统。一个简单的,使用面向过 程的设计方法开发的GUI系统如图5所示。从图5中可以看出,应用程序调用GUI框架中的C

15、reateWindow()函数来创建窗口,在 这里,我们可以说应用程序依赖于GUI框架。但GUI框架并不了解该窗口接收到窗口消息后 应该如何处理,这一点只有应用程序最为清楚。因此,当GUI框架需要发送窗口消息时,又 必须调用应用程序定义的某个特定的窗口函数(如上图中的MyWindowProc)。这时,GUI 框架又必须依赖于应用程序。这是一个典型的双向依赖关系。这种双向依赖关系有一个非常 严重的缺陷:由于GUI框架调用了应用程序中的某个特定函数(MyWindowProc), GUI框 架根本无法独立存在;换一个新的应用程序,GUI框架多半就要做相应的修改。因此,如何 消解框架系统对应用程序的依

16、赖关系是实现框架系统的关键。并非只有面向对象的方法才能解决这一问题。WIN32 API早就为我们提供了在面向过程的设计思路下解决类似问题的范例。类WIN32的架构模型如图6所示。在图6中,应用程序调用CreateWindow()函数时,要传递一个消息处理函数的指针给 GUI框架(对WIN32而言,我们在注册窗口类时传递这一指针),GUI框架把该指针记录在 窗口信息结构中。需要发送窗口消息时,GUI框架就通过该指针调用窗口函数。和图5相 比,GUI框架仍然需要调用应用程序,但这一调用从一个硬编码的函数调用变成了一个由应 用程序事先注册被调用对象的动态调用。图6用一条虚线表示这种动态调用。可以看出

17、, 这种动态的调用关系有一个非常大的好处:当应用程序发生变化时,它可以自行改变框架系 统的调用目标,GUI框架无需随之发生变化。现在,我们可以说,虽然还存在着从GUI框架 到应用程序的调用关系,但GUI框架已经完全不再依赖于应用程序了。这种动态调用机制通 常也被称为“回调函数”。在面向对象领域,“回调函数”的替代物就是“模板方法模式”,也就是“好莱坞原则 (不要调用我们,让我们调用你)”。GUI框架的一个面向对象的实现如图7所示。Wf I! .人山*nd .佃图?通过模械方法模式消解GUI框架登应用程序的依熬应用程序7卷桀5*OeaiBV:ifiQ?w)主甫恩悔阪)主清恿慵环IH说扑 I汩(G

18、eMssatei . J (心.E访I.小:冲.Win :“臻3ES.-K3-* * H i*盘打客:;睥谬 Cr KeybaaclRea(Jer(), writer = n&w PnnterWnter();cpy(= (汨;c:k4r说 i(c = !i=r :ead( :J t= I of -Kirt: :i F?购川)从图8中可以看出,虽然Reader和Writer接口隔离了 “服务类”和具体的Reader和 Writer类,使它们之间的耦合降到了最小。但当“服务类”创建具体的Reader和Writer 对象时,“服务类”还是和具体的Reader和Writer对象发生了依赖关系一一图8中

19、用蓝 色的虚线描述了这种依赖关系。在这种情况下,如何实例化具体的Reader和Writer类,同时又尽量减少服务类对它们 的依赖,就是一个非常关键的问题了。如果服务类位于应用程序中,这一依赖关系对我们造 成的影响还不算大。但当“服务类”位于需要独立发布的类库中,它的代码就不能随着应用 程序的变化而改变了。这也意味着,如果“服务类”过度依赖于具体的Reader和Writer 类,用户就无法自行添加新的Reader和Writer的实现了。解决这一问题的方法是“依赖注入”,即切断“服务类”到具体的Reader和Writer 类之间的依赖关系,而由应用程序来注入这一依赖关系。如图9所示。图3依赖注入在

20、图9中,“服务类”并不负责创建具体的Reader和Writer类的实例对象,而是由 应用程序来创建。应用程序创建“服务类”的实例对象时,把具体的Reader和Write对象 的引用注入“服务类”内部。这样,“服务类”中的代码就只和抽象接口相关的了。具体实 现代码发生变化时,“服务类”不会发生任何变化。添加新的实现时,也只需要改变应用程 序的代码,就可以定义并使用新的Reader和Writer类,这种依赖注入方式通常也被称为“构 造器注入”。如果专门为Copy类抽象出一个注入接口,应用程序通过接口注入依赖关系,这种注入 方式通常被称为“接口注入”。如果为Copy类提供一个设值函数,应用程序通过调

21、用设值函数来注入依赖关系,这种依赖注入的方法被称为“设值注入”。具体的“接口注入”和 “设值注入”请参考Martin 2004。PicoContainer和Spring轻量级容器框架都提供了相应的机制来帮助用户实现各种不 同的“依赖注入”。并且,通过不同的方式,他们也都支持在XML文件中定义依赖关系,然 后由应用程序调用框架来注入依赖关系,当依赖关系需要发生变化时,只要修改相应的XML 文件即可。因此,依赖注入的核心思想是:1. 抽象接口隔离了使用者和实现之间的依赖关系,但创建具体实现类的实例对象仍会 造成对于具体实现的依赖。2. 采用依赖注入可以消除这种创建依赖性。使用依赖注入后,某些类完全

22、是基于抽象 接口编写而成的,这可以最大限度地适应需求的变化。结论分离接口和实现是人们有效地控制依赖关系的最初尝试,而纯粹的抽象接口更好地隔离 了相互依赖的两个模块,“依赖倒置”和“控制反转”原则从不同的角度描述了利用抽象 接口消解耦合的动机,GoF的设计模式正是这一动机的完美体现。具体类的创建过程是另一 种常见的依赖关系,“依赖注入”模式可以把具体类的创建过程集中到合适的位置,这一动 机和GoF的创建型模式有相似之处。这些原则对我们的实践有很好的指导作用,但它们不是圣经,在不同的场合可能会有不 同的变化,我们应该在开发过程中根据需求变化的可能性灵活运用。关键字:依赖关系宣战倒置控制反转|来源:普索网|责任编辑:系统管理员|最后 编辑:2011年04月12日22时44分05秒相关阅读

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 生活休闲 > 在线阅读


备案号:宁ICP备20000045号-2

经营许可证:宁B2-20210002

宁公网安备 64010402000987号