依赖倒置原则(The Dependency Inversion principle)
注:这是Robert C. Martin 1996,在C++ Report上发表的文章,他认为:依赖倒置原理是面向对象技术宣称的很多优越性的根源,也是设计模式(design patterns)的基石。原文见:http://www.objectmentor.com/resources/articles/dip.pdf
作为作业让吴兰芳、金艳、马婧、段亚岚、阮珊、佟哲翻译,最后yqj2065做了一些修改。
注2:2019.12,我将逐字逐句地指出其错误。为了阅读方便,翻译中【批评】里的咚咚,是我的批注。
这是我【Robert C. Martin】为C++报导(The C++ Report)所写的工程笔记专栏的第3篇文章,将在本栏目里刊登的这篇文章将主要讨论C++和OOD的使用,和软件工程上的一些要点。我将争取写出对软件工程师有实效而且直接有用的文章。在这些文章里,我将用Booch和Rumbaugh新提出的统一符号(Version 0.8)【UML的前身】建档面向对象设计。旁边这个图提供一个该标识法的简要说明 (图略)。
§1引言
我的上一篇文章(1996-3)讨论了里氏替换原则(Liskov Substitution Principle 、LSP)。这个原则,被应用于C++时,为公有继承的使用提供了指导。其阐明:每一个函数,运用(操作)在基类的引用或指针之上时,就应该能够运用在该基类的派生类上,(甚至)不需要知道派生类为何物。这意味着:子类的虚函数必须指望它们不多于基类的相应函数,同时要保证不少于(基类的相应函数)。这也意味着基类中呈现的虚拟成员函数必须在衍生类中出现,而且它们必须能做有用的事儿。当这个原则被违反时,运用在基类的引用或指针之上的函数就需要检查该当前对象(the actual object)的类型,以保证它们(这些函数)能够正确的运用在其【这个实际对象】之上。而这——需要去检查类型,就违背了开闭原则(OCP),我们在去年1月就讨论过了。
在这个专栏(文章)里,我们讨论OCP和LSP的结构推断(the structural implications)。这个结构——作为严格使用这些原则的结果——能被概括为一条原则,我称其为"依赖倒置原则"(DIP)。【Robert C. Martin :I first stumbled on this principle when Jim Newkirk and I were arranging the source code directories of a C++ project. We realized that we could make the directories that contained detailed code depend upon the directories that contained abstract classes. This seemed like an inversion to me, so I coined the name "Dependency Inversion".】
§2软件出了什么毛病?
我们大多数人都有这样不愉快的经历,试图处理一些"坏设计"的软件片断。有些人甚至更不愉快的体验,发现我们正是"坏设计"软件的作者。是什么造成了糟糕的设计?
大多数软件工程师并不以创建"坏设计"为出发点,然而大多数软件最终沦落到这个地步,被某人宣判为设计不健全。这又是如何发生的?是一开始就是糟糕的设计,还是设计居然会变质——就象坏了的肉一样?这个议题的核心在于我们缺乏合适的定义:什么是"坏"设计。【】
“坏设计”的定义
你是否曾经展示过一种自己特感骄傲的软件设计,让同伴评论?那些同伴有没有用一种嘲笑的语气抱怨,例如“你为什么要用那种方式做这件事呢”?这种事真的在我身上发生过,我也看见它发生在很多其他工程师身上。无疑地,意见不一的工程师没有使用相同的标准去定义何谓之“坏设计”。我见过的使用得最普遍的标准是TNTWIWHDI,就是说"那不是我去做时会使用的方式"(That’s not the way I would have done it)。
但是,这里有一些标准,我相信所有工程师都会认同。软件片断(虽然)符合其需求(fulfills its requirements),但因(yet)表现出以下3种特性中的一些或全部,那就是“坏设计”。
1. 改变起来很难,因为每种变化都会影响系统的太多其他部分。(Rigidity刚性、僵硬)。
2. 当你作了一个变动时,系统中意想不到的部分会出错。 (Fragility、易碎性)
3. 它难以在另一个应用程序中复用,由于它不能脱离当前应用。(Immobility、固定、无移植性)
此外,很难例证(demonstrate)某个软件片断在没有任一上述特征时,也就是说,它是灵活的(flexible),鲁棒的(robust)和可复用的(reusable)而且符合其需求,会是一个“坏设计”。因此,我们能使用这3种特性作为确切判定一种设计是"好"或者"坏"的一种方法。
导致“坏设计”的原因
是什么导致设计刚性(rigid)、脆弱(fragile)和不易移植(immobile)的呢?它(原因)就是该设计中模块的相互依赖(interdependence)。这个设计就是刚性的,如果它不能容易地被改变,这样的刚性是因为这一事实——对于严重相互依赖的软件,单个变化引起了依赖模块中的级联变化(a cascade of changes、连锁反应)。一旦级联变化的范围不能被设计者或者维护者预先知道,变化的影响就不能被估计。这导致变化的开销不可能被预言。管理层面对如此不可预测性,变得不愿意批准变动。因此,设计就官方上(正式、officially)地成为刚性。
脆弱性(易碎性、Fragility)是一种单个变化发生时,程序在很多地方中断的趋势。经常的,新问题出现在与被改变的领域没有概念上的关系的地方。这样的易碎性极大降低了设计的可信性(credibility)和可维护性(maintenance organization)。用户和管理者不能预言他们的产品的质量。应用的某个部分的简单变化导致在看起来完全无关的其他部分出现失败。解决那些问题导致甚至更多的问题,而维护工作开始(变得)像一条狗追赶它尾巴。
设计是不易移植的,是说设计中想要的部分高度地依赖于不太想要的细节。一些设计者被分配的任务是研究(调查)设计,看看它能否在不同的应用中复用,他们会惊叹于该设计能够那么好的复用于新的应用中(Designers tasked with investigating the design to see if it can be reused in a different application may be impressed with how well the design would do in the new application.)然而,如果一个设计是高度互相依赖的,他们就会非常的苦恼,把该设计中想要的部分与该设计中不想要的部分分开,有大量的工作必须做。多数情况下,这样的设计是不被复用的,因为分离的费用被认为要高于重新设计(redevelopment of the design)的费用。【依赖于抽象类型不过是OCP在类上的应用。目前养狗养猫,为了应对需求变化,就使用一个大概念(父类型宠物)覆盖未来可能会添加的类如养兔子,依赖的父类型应该是抽象类型。就是这么简单地逻辑,非要研究什么是 坏设计干嘛?】
例子:“Copy”程序
一个简单的例子可以帮助理解这一点。考虑一个承担下面任务的简单程序:copy键盘上输入的字符,在打印机上输出。假定,实施平台并没有一个操作系统去支持设备无关性。我们可以想象出这个程序的结构就像是图1。
图1就是一个"结构图"(structure chart)[1]。(1.See:The Practical Guide To Structured Systems Design , by Meilir Page-Jones, Yourdon Press,1988)它表明在应用中有3个模块或者子程序。Copy模块调用其他的两个模块。人们很容易想象,在Copy模块内有一个循环(见清单1.)。该循环体中调用Read Keyboard模块从键盘那里获取一个字符,它然后将那个字符发送到Write Printer模块以打印该字符。
清单1:“复制”程序
void Copy(){
int c;
while ((c = ReadKeyboard()) != EOF)
WritePrinter(c);
}
这两个低层模块具有好的复用性。它们可以用在很多其他程序里,以访问键盘和打印机。这和我们从子程序库中获得的复用性是相同。但是,在不包括键盘或者打印机的任何环境中,“Copy”模块是不可复用的。让人害羞的是,系统的智能就靠这个模块来维系。正是Copy模块封装了我们想复用的非常有趣的策略(policy)。
例如,考虑一个新程序,它把键盘输入的字符复制到磁盘文件。显然,我们希望复用Copy模块,因为它封装了我们需要的高层功能,就是说,它知道如何从一个来源复制字符到一个接受器。不幸的是,Copy模块依赖于Write ,Printer模块,因此不能在新上下文里被复用。当然,我们可以修改Copy模块以赋予它新的所希望的功能性(见清单2)。
清单2:“增强”的“复制”程序
enum OutputDevice {printer, disk};
void Copy(outputDevice dev){
int c;
while ((c = ReadKeyboard()) != EOF)
if (dev == printer)
WritePrinter(c);
else
WriteDisk(c);
}
我们在其中增加一个if语句,它依赖某种标志,在Write Printer 模块和Write Disk模块之间做出选择。但是,这就给系统添加了新的依赖。随着时间的推移,越来越多的设备必须加入到Copy程序中,该Copy模块塞满了if/else语句,并且将依赖很多较低层的模块。最终它变得硬而脆。
【“例子”部分,高层-低层模块(High -low level modules)比较含混,使用Client-IServer来进行一般性的讨论更清晰,如果说他错误,有咬文嚼字的嫌疑。客户Client不应该依赖具体类Server,应该依赖抽象类型IServer,不是简单明了吗?
真正的问题是,“Copy模块封装了我们想复用的非常有趣的策略”,但是Copy不适合设计成框架。在后面,高层-低层模块又用于分层架构中,为什么不要含义清晰的上层-下层模块,框架属于下层,应用程序属于上层。
他的例子,用清晰的语言说,就是作为Client的Coyp如果依赖具体的Server(即Write ,Printe)不好。】
§3 依赖倒置
描述上述问题的一种方式是,注意包含高层功能的模块(如copy()模块)依赖于它所控制的低层更细节的模块(例如:. WritePrinter() 和 ReadKeyboard())。如果我们能找到新的途径使copy()模块不依赖于它控制的细节,那么,我们就能自由地复用它。我们能开发出其它的程序,其中使用这个模块从任何输入装置复制字符到任何输出装置。OOD给了我们一个机制以实现这种依赖倒置。
【就这?Client从依赖具体类Server变成依赖IServer,就是依赖倒置?如果真是这样,真的不要用inversion】
清单3:面向对象的“复制”程序
class Reader{
public:
virtual int Read() = 0;
};
class Writer{
public:
virtual void Write(char) = 0;
};
void Copy(Reader& r, Writer& w)
{
int c;
while((c=r.Read()) != EOF)
w.Write(c);
}
}
参见图2点简单类图,这里Copy类包含了抽象Reader类和抽象Writer类,压根的既不依赖于Keyboard Reader也不依赖于Printer Writer。因此,依赖性已经被倒置;Copy类依赖于抽象(抽象类),而具体的读取器和写出器依赖相同的抽象。
现在,我们能够复用Copy类了,它不依赖于“Keyboard Reader” 和 “Printer Writer”。我们能发明各种新的Reader和Writer的派生物,以支持我们的Copy类。更绝的是,不管多少种(具体的)读取器和写出器被创造出来,Copy类将不会依赖它们中任何一个。这里没有相互依赖性去导致设计变得刚性或者脆弱,而Copy()【函数?】本身可以被很多不同的上下文使用。它是可移植的。
【因此依赖性被反转了!如果读到这里不再往下看,Client从依赖具体类Server变成依赖IServer,就是他的所谓依赖反转,可以把它作为针对接口编程/抽象依赖原则的同义词。】
设备独立性
到这里,或许有人喃喃自语,我能通过使用stdio.h固有的设备独立性(即getchar和putchar),用C编写Copy()函数以达到相同的效果(见清单4)。
清单4:使用stdio.h的“复制”程序
#include <stdio.h>
void Copy(){
int c;
while((c = getchar()) != EOF)
putchar(c);
}
如果你仔细考虑清单3和4,你将意识到两者是逻辑等效的。在图3中的抽象的类被清单4中另一种不同的抽象所替换。的确,在清单4没有使用类和纯虚函数(pure virtual functions),然而它仍然使用了抽象和多态达到目的。而且,它仍然使用依赖倒置!在清单4中Copy程序不依赖任何其控制的细节,相反它依赖在stdio.h里声明的抽象设备。而且,最终被调用的IO设备也依赖在stdio.h里声明的抽象。因此,在stdio.h库内的设备独立性是依赖倒置另一例子。
既然我们已经见了一些例子,我们能说明DIP的一般形式。
§4 依赖倒置原则
A .高层模块不应该依赖低层模块。两个都应该依赖抽象。
B .抽象不应该依赖细节。细节应该依赖抽象。
【正是因为前面的依赖反转,很多人把DIP认为是针对接口编程/抽象依赖原则的花哨的版本。你要注意,在他的行文中,出现的一些词:它所控制的,Inversion/反转/倒置。这是为“反转 ”3号做准备】
有人可能会问,为什么我要使用单词"倒置"(“inversion”.)。坦白地说,这是因为比较传统的软件开发方法——例如结构化分析和设计,倾向于创建这样的软件结构:高层模块依赖于低层模块,并且抽象依赖细节。的确,这些方法的一个目标在于定义一个子程序层次以描述高层模块如何调用低层模块,图1是一个这样层次的好例子。因此,一个设计良好的面向对象程序的依赖结构,对应于传统的过程式方法通常会形成的依赖结构,是"倒置"的。
【有人可能会问,为什么我要使用单词"倒置"(“inversion”.)。坦白地说,我是想介绍IoC。Robert C. Martin应该这样写。
好像传统的过程式方法就不能够设计框架一样,应用程序调用库函数 和 框架设计是不同的应用方式,没有好坏之分。而依赖抽象类型原则,与软件是不是采用“传统的软件开发方法”、“过程式方法”或“面向对象”,一点关系都没有。
C语言也可以通过函数指针到达依赖抽象(类型)的目的。
在胡乱解释反转时,就会漏洞百出。“反转 ”2号:对传统的软件开发方法的反转。把女人中的好人和男人中的坏人加以比较,得到结论:女人与男人在人性上是倒置的,显然是极其荒谬的。
DIP所言的倒置2号,本身就是胡扯,因此其鼓吹者不得不随意发挥,如《Head First 设计模式》对依赖倒置的解说甚至是“倒置你的思考方式”。】
想想高层模块依赖于低层模块的寓意。是高层模块包含一个应用的重要策略决定和商业模式,是这些模块包含应用的身份。然而,当这些模块依赖较低级的模块,低层模块的改变就对它们有直接的影响,并且迫使他们去改变。
【在Robert C. Martin的潜意识里,底层模块都是具体的。好像所有的底层模块都是Server,等着DIP来拯救】
这种困境是荒谬的!是高层模块应该迫使低层模块改变。是高层模块应该优于低层模块。无论如何,高层模块完全不应该依赖低层模块。
而且,高层模块才是我们想要复用的。我们已经十分擅长以子程序库的形式复用低层模块。当高层模块依赖低层模块,在不同的上下文中复用那些高层模块就非常困难。另一方面,当高层模块不依赖低层模块时,高层模块可以被十分简单复用。
正是这个原则,它是框架设计(framework design.)的核心。
【Robert C. Martin为了其原则,预设了一个前提,高层应该设计成框架。
但是Robert C. Martin自己给出的例子,Copy模块,Java程序员都知道,几乎没有人为特定的应用开发自己的IO框架,而是使用Java的IO工具箱构造各种各样的应用。在很多场合,所谓的高层模块通常不具备复用性。
在框架的场合,SortTest→IntSort例子中,高层模块SortTest的确具有作为框架的可能性;不论在应用程序场合,还是框架场合,可能的设计都是SortTest→IntSort。依赖关系没有任何变化。】
分层(Layering)
按照Booch的说法, "所有良好结构化的面向对象架构(well structured object-oriented architecture)都有定义清楚的层次,通过【though似乎应该是through】定义良好及受控的接口(interface)使每个层提供某种相关的一些服务"。
图3:简单的分层
【再说一遍:在Robert C. Martin的潜意识里,底层模块都是具体的。所以他这里说的东西,都是废话!
前面的反转,他自己也觉得不好意思。于是,他需要在分层结构中,在心中将他的反转伪装成控制反转/IoC中的反转。这才是他真正想表达的东西,但是他却只字不提。我将它补上,称为“反转 ”3号。
】
这句话的一种幼稚的(naive)解释会导致设计者搞出一个类似于图3的结构。在这张图解里,高层的类policy(策略)使用一个较低层的Mechanism(机制);(后者)依次的使用一个细节的层utility(工具)类。这种情况也许看起来是恰当的,但这里存在一个随时会引爆的地雷(the insidious characteristic)——policy层对于深入到Utility层内的所有方式的改变都是敏感的。(因为)依赖是可传递的。policy层依赖某些东西,而某些东西又依赖utility层,故而policy层传递性的依赖utility层,这非常不幸。
图4显示一个更合适的模型。每个较低级别的层由一种抽象类(abstract class)描述,而实际层从这些抽象类派生。每种较高级的类通过该抽象接口使用下一个较低的层次。因此,每个层都不依赖任何其它层。相反,层依赖抽象类。不仅Policy层对utility层的传递性依赖被断开,甚至Policy层对Mechanism层的直接依赖也被断开。
使用本模型,Mechanism层或者utility层任何变化都不会影响Policy层,而且,对于所定义的符合Mechanism层接口的低层模块之任何上下文中,Policy层都能复用。因此,通过倒置依赖性,我们就构建了兼有更灵活,更耐用,可移植性更强的结构。
C++中的接口与实现相分离
【现在,Robert C. Martin的DIP中,还有一个东西要介绍,什么是抽象-细节的对应关系?既然DIP的第一条已经说明低层模块(子类型)要依赖抽象,为什么还要再次说明 “抽象不应该依赖细节,细节应该依赖抽象”呢?】
有人会抱怨道,图3中的结构并不存在我声称的依赖和传递性依赖问题,不管怎么说,Policy层仅仅依赖于Mechanism层的接口。Mechanism层的实现(implementation)的变化怎么会向上最终影响到Policy层呢?
在一些面向对象语言中,这可能是真的。在那些语言中,接口自动的从实现中分离开来。然而,在C++中,接口与实现并不存在分离。准确的说,C++中,分离的是类的定义和它的成员函数的定义。
C++中,我们通常把一个类分为两块,a.h和a.cc;.h文件包含类的定义。. cc文件包含类的成员函数的定义。类的定义——在.h文件中,还包含了所有类的所有成员函数和所有成员变量的声明。这种信息超出了简单的接口。类所需要的所有的功能函数(utility functions)和私有变量也都在.h文件中声明。这些功能和私有变量是类的实现的一部分,而它们出现在其中的模块是该类的所有用户都必须是依赖的。因此,C++中,实现并不是自动与接口相分离的。
C++中,接口与实现相分离的缺欠,能够使用纯粹的抽象的类(purely abstract class)来处理。纯抽象类是这样的类,它除了包含纯虚函数外,一无所有。这样的类才是纯粹的接口,并且它的.h文件不包含实现。图4展示了这一结构。图4中的抽象类都意味者是纯抽象(类)以使得每个层仅仅依赖于子层的接口。
【Robert C. Martin希望用自己的术语抽象-细节,替换“针对接口编程,而不是针对实现编程”中的接口-实现,正如抽象依赖原则中用抽象类型-具体类型替换接口-实现一样。抽象依赖原则之所以要替代,因为这个接口-实现,容易与Parnas原则即接口与实现的分离原则中的接口-实现混淆。
Parnas原则说明用户只需要了解接口,因此可以对实现视而不见。Java的抽象方法和C++的纯虚函数可以定义单纯的接口。而Robert C. Martin对Parnas原则的理解,是接口与实现在物理上的分离。因此他在“C++中的接口与实现相分离”一节中,写到:『C++中,实现并不是自动与接口相分离的。…C++中,接口与实现相分离的缺欠,能够使用纯粹的抽象的类(purely abstract class)来处理。』,换言之,只有Java的接口(Java8之前)才能够满足他的接口与实现分离。
显然,他不懂什么是接口与实现分离。】
§一个简单的例子
无论在哪里,一个类向另一个类发送消息时都能应用依赖倒置。例如,考虑一下Button对象和Lamp对象的案例。
Button(按钮)对象能感知外部环境。它能测定用户是已经“按”还是没有按它。这种感知的机制是什么并不重要。它可能是GUI上的一个button图标,或者被人的指头按的物理按钮,甚至是家用安全系统的运动探测器。Button(按钮)对象探测用户是激活它还是使其失效(开或关)。Lamp(灯)对象感受(affects)外部环境(的刺激)。一旦接受到TurnOn(打开)的消息,灯就发出某种形式的光。当接受到TurnOff(关灯)消息,就熄灭该光。其物理机制不重要,它可能是电脑控制台上的一个LED,停车场的mercury vapor lamp或是一个激光打印机的激光。
我们怎样设计一个系统使Button对象控制Lamp对象呢?图5展示了一个幼稚的模型。Button对象简单的发送TurnOn和TurnOff消息给Lamp对象。方便起见,Button类使用“包含”的关系来拥有一个Lamp类的实例。清单五显示了这个模型的C++代码。
图5:幼稚的按钮/灯模型
【这种简单地例子不需要图,不需要仔细看他的源代码。幼稚的模型是Button→Lamp。】
清单5:幼稚的按钮/灯代码
--------------lamp.h----------------
class Lamp {
public:
void TurnOn();
void TurnOff();
};
-------------button.h---------------
class Lamp;
class Button{
public:
Button(Lamp& l) : itsLamp(&l) {}
void Detect();
private:
Lamp* itsLamp;
};
-------------button.cc--------------
#include “button.h”
#include “lamp.h”
void Button::Detect(){
bool buttonOn = GetPhysicalState();
if (buttonOn)
itsLamp->TurnOn();
else
itsLamp->TurnOff();
}
注意:Button类是直接依赖于Lamp类的。实际上,button.cc文件#include了lamp.h文件。这种依赖性暗示,无论Lamp类何时改变,Button类就必须改变,或者至少要重新编译。而且,复用Button类来操作马达对象是不可能的。图5和列表5违反了依赖倒置原则。应用的高层(policy)没有与低层模块相分离;抽象没有与细节相分离。没有这样的分离,高层自动的依赖于低层模块,并且抽象也自动的依赖于细节。
发掘潜在的抽象
什么是高水平的策略?正是抽象作为应用的基础,原理(truths)是不随细节的变化而改变。在Button/Lamp的例子中,潜在的抽象是识别出用户的on/off信号并将这个信号传递到目标对象。识别用户信号的机制是什么?管它呢!目标物体是什么?管它呢!这些都是不影响抽象的枝末细节。
为了与依赖倒置原则相符,我们必须把这种抽象与问题的细节相隔离。因而我们必须指导依赖性的设计,让细节依赖于抽象。图6显示了这种设计。
【但是,接口与实现分离不是其想说的要点。『抽象没有与细节相分离。没有这样的分离...并且抽象也自动的依赖于细节...为了与依赖倒置原则相符,我们必须把这种抽象与问题的细节相隔离。因而我们必须指导依赖性的设计,让细节依赖于抽象。』等等,不过是说,要把具体依赖如Button→Lamp,改成Button=>IBButton→IButtonClient<= Lamp。最后,他的抽象-细节又与抽象类型-实现类等价。
你到底想说什么?如果抽象-细节是IServer和Server,IServer不可能依赖Server,如同你说"水不应该向上流",水从来就没有向上流过!
唯一的优点是,令抽象=A,细节=B,这个话就是“A不应该依赖B,B应该依赖A”,完美的依赖倒置——虽然永远不存在。
如果你直接说子类型依赖抽象的父类型,那他还玩个屁啊。这就是原则B的真正含义,一句简单地话,都能够包装成令很多人脑洞大开的原则。有些人开始发挥:抽象是稳定的......】
图6中,我们已经将Button类的抽象从它的详细实现中隔离出来。列表6给出了相应的代码。注意:高层的policy完全封装(captured)在抽象Button类中,Button类对检测用户信号的物理机械作用全然不知;对lamp更是一无所知。这些细节隔离到具体的派生物中:ButtonImplementation和Lamp。
列表6中高层策略能够复用到任何按钮上,以及任何类型的需要控制的设备。而且,它不受低层机制变化的影响。因此它在变化面前是鲁棒的,灵活的,且可复用的。
----------byttonClient.h---------
class ButtonClient
{
public:
virtual void TurnOn() = 0;
virtual void TurnOff() = 0;
};
-----------button.h---------------
class ButtonClient;
class Button
{
public:
Button(ButtonClient&);
void Detect();
virtual bool GetState() = 0;
private:
ButtonClient* itsClient;
};
---------button.cc----------------
#include button.h
#include buttonClient.h
Button::Button(ButtonClient& bc)
: itsClient(&bc) {}
void Button::Detect()
{
bool buttonOn = GetState();
if (buttonOn)
itsClient->TurnOn();
else
itsClient->TurnOff();
}
-----------lamp.h----------------
class Lamp : public ButtonClient
{
public:
virtual void TurnOn();
virtual void TurnOff();
};
---------buttonImp.h-------------
class ButtonImplementation
: public Button
{
public:
ButtonImplementaton(
ButtonClient&);
virtual bool GetState();
};
抽象的更进一步扩展
人们(Once? ones)能对Figure/Listing 6的设计提出正常的埋怨。由button控制的设备必须派生于ButtonClient。如果Lamp类来自第三方库,并且我们不能修改源代码该怎么办。
图7:Lamp适配器
图7示范了如何使用适配器模式将第三方Lamp对象连接到模块上的。LampAdapter类简单地将从ButtonClient中继承的TurnOn和TurnOff消息转化为Lamp类需要明白的那些消息。
结论
依赖倒置原理是面向对象技术宣称的很多优越性的根源。对其适当的应用是创造可复用框架所必要的。要想构造出对变化富于弹性的代码,该原理也极其重要。既然抽象和细节彼此被完全隔离,代码就非常容易维护。
这篇文章是我的新书——《模式和面向对象设计高级原则》,很快将由Prentice Hall出版——中一章的高度浓缩版本。在后面的文章中,我们将探索面向对象设计许多其他原理。我们还将会学习各种设计模式(design patterns),以及它们联系到C++实现时的强大或薄弱之处。我们还将在C++中学习Booch的类理论(class categories),它们作为C++命名空间(namespaces)的适用性。我们还将定义OOD中什么是"内聚性(cohesion)"(cohesion)和"耦合"(coupling),并且我们将会开发出衡量面向对象设计质量的方法学(metrics)。此后,我们将会讨论许多其他感兴趣的论题。