依赖倒置原则(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 char