一篇依赖倒置,控制反转,依赖注入好文
依赖和耦合(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()为函数库中的两个函数,应用程序循环调用这两个函数,以便把用户键入的字符拷贝到打印机输出。
? 不依赖于函数库的具体实现,C语言把函数的定义写在了一个分离的头文件(函数库.h)中。这种做法的好处是:虽然应用程序要调用函数库、依赖于函数库,但是,当我们要改变函数库的实现时,只要重写函数的实现代码,应用程序无需发生变化。例如,改变函数库.c文件,把WritePrinter()函数重新实现成向磁盘中输出,这时只要将应用程序和函数库重新链接,程序的功能就会发生相应的变化。 ? 一般情况下,由于类库的设计者并不知道应用程序会如何使用类库,抽象接口大多由类库设计者根据自己设想的典型使用模式总结出来,并保留一定的灵活度,以提供给应用程序的开发者使用。 控制反转(Inversion of Control)
在图 6中,应用程序调用CreateWindow()函数时,要传递一个消息处理函数的指针给GUI框架(对WIN32而言,我们在注册窗口类时传递这一指针),GUI框架把该指针记录在窗口信息结构中。需要发送窗口消息时,GUI框架就通过该指针调用窗口函数。和图 5 相比,GUI框架仍然需要调用应用程序,但这一调用从一个硬编码的函数调用变成了一个由应用程序事先注册被调用对象的动态调用。图 6用一条虚线表示这种动态调用。可以看出,这种动态的调用关系有一个非常大的好处:当应用程序发生变化时,它可以自行改变框架系统的调用目标,GUI框架无需随之发生变化。现在,我们可以说,虽然还存在着从GUI框架到应用程序的调用关系,但GUI框架已经完全不再依赖于应用程序了。这种动态调用机制通常也被称为“回调函数”。
图 7中,“GUI框架抽象接口”是GUI框架系统提供给应用程序使用的接口。抽象出该接口的动机是根据“依赖倒置”的原则,消解从应用程序到GUI框架之间的直接依赖关系,以使得GUI框架实现的变化对应用程序的影响最小化。Window接口类则是“模板方法模式”的核心。应用程序调用CreateWindow()函数时,GUI框架会把该窗口的引用保存在窗口链表中。需要发送窗口消息时,GUI框架就调用窗口对象的SendMessage()函数,该函数是实现在Window类中的非虚成员函数。SendMessage()函数又调用WindowProc()虚函数,这里实际执行的是应用程序MyWindow类中实现的WindowProc()函数。在图 7中,我们已经看不到从GUI框架到应用程序之间的直接依赖关系了。因此,模板方法模式完全实现了回调函数的动态调用机制,消解了从框架到应用程序之间的依赖关系。
在前面的例子里,我们通过“依赖倒置”原则,最大限度地减弱了应用程序Copy类和类库提供的服务Read,Write之间的依赖关系。但是,如果需要把Copy()函数也实现在类库中,又会发生什么情况呢?假设在类库中实现一个“服务类”,“服务类”提供Copy()方法供应用程序使用。应用程序使用时,首先创建“服务类”的实例,调用其中的Copy()函数。“服务类”的实例初始化时会创建KeyboardReader 和PrinterWriter类的实例对象。如图 8所示。 从图 8中可以看出,虽然Reader和Writer接口隔离了“服务类”和具体的Reader和Writer类,使它们之间的耦合降到了最小。但当 “服务类”创建具体的Reader和Writer对象时,“服务类”还是和具体的Reader和Writer对象发生了依赖关系——图 8中用蓝色的虚线描述了这种依赖关系。 在图 9中,“服务类”并不负责创建具体的Reader和Writer类的实例对象,而是由应用程序来创建。应用程序创建“服务类”的实例对象时,把具体的Reader和Write对象的引用注入“服务类”内部。这样,“服务类”中的代码就只和抽象接口相关的了。具体实现代码发生变化时,“服务类”不会发生任何变化。添加新的实现时,也只需要改变应用程序的代码,就可以定义并使用新的Reader和Writer类,这种依赖注入方式通常也被称为“构造器注入”。
分离接口和实现是人们有效地控制依赖关系的最初尝试,而纯粹的抽象接口更好地隔离了相互依赖的两个模块,“依赖倒置”和 “控制反转”原则从不同的角度描述了利用抽象接口消解耦合的动机,GoF的设计模式正是这一动机的完美体现。具体类的创建过程是另一种常见的依赖关系,“依赖注入”模式可以把具体类的创建过程集中到合适的位置,这一动机和GoF的创建型模式有相似之处。 本文转自流星的Blog (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |