- 论坛徽章:
- 0
|
【注:这是Robert C. Martin 1996——有一千年了,在C++ Report上发表的文章,影响深远。依赖倒置原理是面向对象技术宣称的很多优越性的根源。也是设计模式(design patterns)的基石。
原文为:
[color="#4060c0"]The Dependency Inversion Principle
(即
http://www.objectmentor.com/resources/articles/dip.pdf
)PDF格式。
***作为作业让吴兰芳、金艳、马婧、段亚岚、阮珊、佟哲翻译,最后yqj2065做了一些修改。一些我搞不定的地方,请大虾指教。为了阅读方便,显示如in the trenches】
文章内【……】里的咚咚,是我的注释。
这是我【Robert C. Martin】的工程笔记专栏C++报导(Engineering Notebook columns for The C++ Report)的第3篇,将在本栏目里刊登的这篇文章将主要讨论C++和OOD的使用,和软件工程上的一些要点。我将争取写出对软件工程师【in the trenches、同一个战壕、兄弟?】有实效而且直接有用的文章。在这些文章里,我将用Booch 和Rumbaugh新提出的统一符号(Version 0.8)【UML的前身】建档面向对象设计。旁边这个图提供一个该标识法的简要说明 (The sidebar provides a brief lexicon of this notation.) 。
file:///E:/OOP%E5%AF%BC%E8%AE%BA%EF%BC%88Java%E6%8F%8F%E8%BF%B0%EF%BC%89/%E5%8E%9F%E7%90%86/%E4%BE%9D%E8%B5%96%E5%80%92%E7%BD%AE%E5%8E%9F%E5%88%99.files/image001.png
边图:统一符号0.8
§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)的费用。
例子:“Copy”程序
file:///E:/OOP%E5%AF%BC%E8%AE%BA%EF%BC%88Java%E6%8F%8F%E8%BF%B0%EF%BC%89/%E5%8E%9F%E7%90%86/%E4%BE%9D%E8%B5%96%E5%80%92%E7%BD%AE%E5%8E%9F%E5%88%99.files/image003.png
图1:“复制”程序
一个简单的例子可以帮助理解这一点。 考虑一个承担下面任务的简单程序:copy键盘上输入的字符,在打印机上输出的。而且假定,实施平台并没有一个操作系统去支持设备无关性。我们可以想象出这个程序的结构就像是图1。图1就是一个"结构图"(structure chart)。(1See: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,功能)。【我们的不希望像C语言那样,处在子程序库的复用水平,我们希望能复用Copy模块,它是我们程序的主要功能模块。】
举例来说,考虑一个新程序,它把键盘输入的字符复制到磁盘文件的。显然,我们希望复用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);
}
我们在其中(its policy??)增加一个if语句,它依赖某种标志在Write Printer 模块和Write Disk 模块之间做出选择。但是,这就给系统添加了新的相互依赖性。慢慢地,当越来越多的设备必须加入到“复制”程序,该Copy模块塞满了if/else语句,并且将依赖很多较低层的模块。最终它变得硬而脆。
§依赖倒置
刻画上述问题的一种方法是,留意到包含高层功能的模块(如copy()模块)依赖于它所控制的低层更细节的模块(例如:. WritePrinter() 和 ReadKeyboard())。如果我们能找到新的途径使copy()模块不依赖于它控制的细节,那么,我们就能自由地复用它。我们能开发出其它的程序,其中使用这个模块从任何输入装置复制字符到任何输出装置。OOD给了我们一个机制以实现这种依赖倒置。
file:///E:/OOP%E5%AF%BC%E8%AE%BA%EF%BC%88Java%E6%8F%8F%E8%BF%B0%EF%BC%89/%E5%8E%9F%E7%90%86/%E4%BE%9D%E8%B5%96%E5%80%92%E7%BD%AE%E5%8E%9F%E5%88%99.files/image005.png
图2:面向对象的“复制”程序
参考图2这个简单的类图(class diagram)。 这里我们有一个Copy类,它包含(contains!***)了一个抽象类Reader和一个抽象类Writer。容易想象,在Copy类中有一个循环,从它的Reader中获取字符,并将字符发送给它的Writer(见清单3)。
清单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);
}
}
此时Copy类压根的既不依赖于Keyboard Reader也不依赖于Printer Writer。因此,依赖性已经被倒置;Copy类依赖于抽象(抽象类),而具体的读取器和写出器依赖相同的抽象。【这里,depends upon用得很随意。】
现在,我们能够复用Copy类了,它不依赖于“Keyboard Reader” 和 “Printer Writer”。 我们能发明各种新的Reader和Writer的派生物,以支持我们的Copy类。更绝的是,不管多少种(具体的)读取器和写出器被创造出来,Copy类将不会依赖它们中任何一个。这里没有相互依赖性去导致设计变得刚性或者脆弱,而Copy()【函数?】本身可以被很多不同的上下文使用。 它是可移植的。
设备独立性
到这里,或许有人喃喃自语,我能通过使用stdio.h固有的设备独立性(即getchar 和putchar),用C编写Copy()函数以达到相同的效果(见清单4)。
清单4:使用stdio.h的“复制”程序
#include
void Copy()
{
int c;
while((c = getchar()) != EOF)
putchar(c);
}
如果你仔细考虑清单3 和4 ,你将意识到两者是逻辑等效的。在图3中的抽象的类被清单4中另一种不同的抽象所替换。的确,在清单4没有使用类和纯虚函数(pure virtual functions),然而它仍然使用了抽象和多态达到目的【呵呵,C的抽象和多态!!!】。而且,它仍然使用依赖倒置!在清单4中Copy程序不依赖任何其控制的细节,相反它依赖在stdio.h里声明的抽象设备。而且,最终被调用的IO设备也依赖在stdio.h里声明的抽象。因此,在stdio.h库内的设备独立性是依赖倒置另一例子。
既然我们已经见了一些例子,我们能说明DIP的一般形式。
file:///C:/Documents%20and%20Settings/Yanqj/My%20Documents/My%20Pictures/nature_039.jpg
§依赖倒置原则
A .高层模块不应该依赖低层模块。两个都应该依赖抽象。
B . 抽象不应该依赖细节。细节应该依赖抽象。
有人可能会问,为什么我要使用单词"倒置"(“inversion”.)。坦白地说,这是因为比较传统的软件开发方法——例如结构化分析和设计,倾向于创建这样的软件结构:<span style="fon
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/5142/showart_16292.html |
|