依赖倒置原则 (依赖倒置原则)
【注:这是罗伯特·C·马丁 1996——有一千年了,在C++报告上发表的文章,影响深远。依赖倒置原理是面向对象技术宣称的很多优越性的根源。也是设计模式(设计模式)的基石。
原文为:依赖倒置原则
(即http://www.objectmentor.com/resources/articles/dip.pdf)PDF格式格式。
***作为作业让吴兰芳、金艳、马婧、段亚岚、阮珊、佟哲翻译,最后yqj2065年做了一些修改。一些我搞不定的地方,请大虾指教。为了阅读方便,显示如在战壕里【参考译文】
文章内【……】里的咚咚,是我的注释。
这是我【罗伯特·C·马丁】的工程笔记专栏C类++报导(C++报表的工程笔记本列)的第三篇,将在本栏目里刊登的这篇文章将主要讨论C类++和OOD公司路线【在战壕里、同一个战壕、兄弟?【参考译文】有实效而且直接有用的文章。在这些文章里,我将用布奇和伦博新提出的统一符号(版本0.8)【统一建模语言的前身】建档面向对象设计。旁边这个图提供一个该标识法的简要说明(侧栏提供了此符号的简短词典。)。
边图:统一符号0.8
§1引言
我的上一篇文章(1996-3)我很喜欢(Liskov替换原则、LSP公司)这个原则,被应用于C类++时间,为公有继承的使用提供了指导。其阐明:每一个函数,运用(操作)在基类的引用或指针之上时,就应该能够运用在该基类的派生类上,(甚至)不需要知道派生类为何物。这意味着:子类的虚函数必须指望它们不多于基类的相应函数,同时要保证不少于(基类的相应函数)。这也意味着基类中呈现的虚拟成员函数必须在衍生类中出现,而且它们必须能做有用的事儿。当这个原则被违反时,运用在基类的引用或指针之上的函数就需要检查该当前对象(实际对象)的类型,以保证它们(这些函数)能够正确的运用在其【这个实际对象】之上。而这——需要去检查类型,就违背了开闭原则(OCP),我们在去年1月就讨论过了。
在这个专栏(文章)里,我们讨论OCP公司和液化石油气的结构推断(结构含义)这个结构——作为严格使用这些原则的结果——能被概括为一条原则,我称其为"赖“(DIP)。【罗伯特·C·马丁 :当我和Jim Newkirk在安排C++项目的源代码目录时,我第一次偶然发现了这个原则。我们意识到,我们可以使包含详细代码的目录依赖于包含抽象类的目录。这对我来说似乎是一种倒置,所以我造币的名称“依赖性反转”。【参考译文】
§2软件出了什么毛病?
我们大多数人都有这样不愉快的经历,试图处理一些"坏设计"【设计得很水】的软件片断。我们中的一些人甚至有更不愉快的体验,发现我们正是"坏设计"软件的作者。是什么造成了糟糕的设计?
大多数软件工程师并不以创建"坏设计"为出发点,然而大多数软件最终沦落到这个地步,被某人宣判为设计不健全。这又是如何发生的?是一开始就是糟糕的设计,还是设计居然会变质——就象坏了的肉一样?这个议题的核心在于我们缺乏合适的定义:什么是"坏"设计。
“坏设计”的定义
你是否曾经展示过一种自己特感骄傲的软件设计,让同伴评论?那些同伴有没有用一种嘲笑的语气抱怨,例如“你为什么要用那种方式做这件事呢”?这种事真的在我身上发生过,我也看见它发生在很多其他工程师身上。无疑地,意见不一的工程师没有使用相同的标准去定义何谓之“坏设计”。我见过的使用得最普遍的标准是TNTWIWHDI公司,就是说"那不是我去做时会使用的方式"(我不会那样做的)
但是,这里有一些标准,我相信所有工程师都会认同。软件片断(虽然)符合其需求(满足其要求),但因(然而)表现出以下三种特性中的一些或全部,那就是“坏设计”。
1 改变起来很难,因为每种变化都会影响系统的太多其他部分。(刚性刚性、僵硬)。
2 当你作了一个变动时,系统中意想不到的部分会出错。(易碎性、易碎性)
三。 它难以在另一个应用程序中复用,由于它不能脱离当前应用。(不可移动性、固定、无移植性)
此外,很难例证(证明)🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂🙂灵活的(灵活的),鲁棒的(坚固耐用的)和可复用的(可重复使用的)而且符合其需求,会是一个“坏设计”。因此,我们能使用这三种特性作为确切判定一种设计是"好"或者"坏"的一种方法。
导致“坏设计”的原因
是什么导致设计刚性(固执的)、脆弱(脆弱的)和不易移植(不动的)的呢?它(原因)就是该设计中模块的相互依赖(相互依存)这个设计就是刚性的,如果它不能容易地被改变,这样的刚性是因为这一事实——对于严重相互依赖的软件,单个变化引起了依赖模块中的级联变化(一连串的变化、连锁反应)一旦级联变化的范围不能被“”“”“”“”“”“”“”“”“”“”“”“”管理层面对如此不可预测性,变得不愿意批准变动。因此,设计就官方上(正式、正式)
脆弱性(易碎性、脆弱性)是一种单个变化发生时,程序在很多地方中断的趋势。经常的,新问题出现在与被改变的领域没有概念上的关系的地方。这样的易碎性极大降低了设计的可信性(可靠性)和可维护性(维修机构)用户和管理者不能预言他们的产品的质量。应用的某个部分的简单变化导致在看起来完全无关的其他部分出现失败。 解决那些问题导致甚至更多的问题,而维护工作开始(变得)像一条狗追赶它尾巴。
中国人(调查)设计,看看它能否在不同的应用中复用,他们会惊叹于该设计能够那么好的复用于新的应用中(负责调查设计以确定其是否可以在不同的应用程序中重用的设计师可能会对该设计在新应用程序中的表现印象深刻。)然而,如果一个设计是高度互相依赖的,他们就会非常的苦恼,把该设计中想要的部分与该设计中不想要的部分分开,有大量的工作必须做。多数情况下,这样的设计是不被复用的,因为分离的费用被认为要高于重新设计(重新开发设计)的费用。
例子:“复制”程序
图1:“复制”程序
一个简单的例子可以帮助理解这一点。 考虑一个承担下面任务的简单程序:复制键盘上输入的字符,在打印机上输出的。而且假定,实施平台并没有一个操作系统去支持设备无关性。我们可以想象出这个程序的结构就像是图1。图1就是一个"结构图"(结构图)。(1请参见:结构化系统设计实用指南梅利尔·佩格·琼斯(Meilir Page-Jones),尤登出版社,1988年) 它表明在应用中有三个模块或者子程序。复制模块调用其他的两个模块。人们很容易想象,在复制模块内有一个循环(见清单1.)。该循环体中调用读取键盘模块从键盘那里获取一个字符,它然后将那个字符发送到写入打印机模块以打印该字符。
清单1:“复制”程序
无效副本()
{
整数c;
while((c=读键盘())!=EOF)
WritePrinter(c);
}
这两个低层模块具有好的复用性。它们可以用在很多其他程序里,以访问键盘和打印机。 获得批准
但是复制模块在不包括键盘或者打印机任何上下文里是不可复用的。让人脸红的是系统的智能就靠这个模块来维持。正是复制模块封装了我们想复用的非常有趣的策略(政策,功能)【我们的不希望像C类语言那样,处在子程序库的复用水平,我们希望能复用复制模块,它是我们程序的主要功能模块。】
举例来说,考虑一个新程序,它把键盘输入的字符复制到磁盘文件的。显然,我们希望复用复制模块,因为它封装了我们需要的高层功能,就是说,它知道如何从一个来源复制字符到一个接受器。不幸的是,复制模块依赖于写入打印机模块,因此不能在新上下文里被复用。
当然,我们能修改复制模块以赋予它新的所希望的功能性(见清单2)。
清单2:“增强”的“复制”程序
enum OutputDevice{打印机,磁盘};
无效副本(outputDevice dev)
{
整数c;
while((c=读键盘())!=EOF)
if(dev==打印机)
WritePrinter(c);
其他的
写磁盘(c);
}
我们在其中(其政策??)一个人如果语句,它依赖某种标志在写入打印机 模块和写入磁盘 模块之间做出选择。但是,这就给系统添加了新的相互依赖性。慢慢地,当越来越多的设备必须加入到“复制”程序,该复制模块塞满了if/else(如果/否则)赖很
§依赖倒置
刻画上述问题的一种方法是,留意到包含高层功能的模块(如复制()模块)依赖于它所控制的低层更细节的模块(例如:.WritePrinter()和读键盘())如果我们能找到新的途径使复制()模块不依赖于它控制的细节,那么,我们就能自由地复用它。我们能开发出其它的程序,其中使用这个模块从任何输入装置复制字符到任何输出装置。OOD公司给了我们一个机制以实现这种依赖倒置。
图2:面向对象的“复制”程序
参考图2这个简单的类图(类图) 这里我们有一个复制类,它包含(包含!***)(笑声)读卡器和一个抽象类作家。容易想象,在复制类中有一个循环,从它的读卡器中获取字符,并将字符发送给它的作家(见清单3)。
清单三:面向对象的“复制”程序
班级阅读器{
公众:
虚拟int Read()=0;
};
类编写器{
公众:
虚拟void写入(char)=0;
};
无效副本(读写器)
{
整数c;
而(c=r.Read())!=EOF)
w.写入(c);
}
}
此时复制类压根的既不依赖于键盘读取器也不依赖于打印机写入器。因此,依赖性已经被倒置;复制类依赖于抽象(抽象类),而具体的读取器和写出器依赖相同的抽象。【这里,取决于用得很随意。】
现在,我们能够复用复制类了,它不依赖于“键盘读取器”和“打印机写入程序”。 我们能发明各种新的读卡器和作家的派生物,以支持我们的复制类。更绝的是,不管多少种(具体的)读取器和写出器被创造出来,复制类将不会依赖它们中任何一个。这里没有相互依赖性去导致设计变得刚性或者脆弱,而复制()【函数?】本身可以被很多不同的上下文使用。 它是可移植的。
设备独立性
到这里,或许有人喃喃自语,我能通过使用标准件。小时固有的设备独立性(即获取字符和输出一个字符)C类编写复制()函数以达到相同的效果(见清单4)。
清单4:使用标准件。小时的“复制”程序
#包括<stdio.h>
无效副本()
{
整数c;
while((c=getchar())!=EOF)
putchar(c);
}
如果你仔细考虑清单三和4,你将意识到两者是逻辑等效的。在图三中的抽象的类被清单4中另一种不同的抽象所替换。的确,在清单4没有使用类和纯虚函数(纯虚函数),然而它仍然使用了抽象和多态达到目的【呵呵,C类的抽象和多态!!!】。而且,它仍然使用依赖倒置!在清单4中复制(英国)不依赖任何其控制的细节,相反它依赖在标准件。小时里声明的抽象设备。而且,最终被调用的IO(输入输出)设备也依赖在标准件。小时里声明的抽象。因此,在标准件。小时库内的设备独立性是依赖倒置另一例子。
既然我们已经见了一些例子,我们能说明DIP公司的一般形式。
§依赖倒置原则
答:。高层模块不应该依赖低层模块。两个都应该依赖抽象。
B、。抽象不应该依赖细节。细节应该依赖抽象。
有人可能会问,为什么我要使用单词"倒置”(“反转”)坦白地说,这是因为比较传统的软件开发方法——例如结构化分析和设计,倾向于创建这样的软件结构:高层模块依赖于低层模块,并且抽象依赖细节。的确,这些方法的一个目标在于定义一个子程序层次以描述高层模块如何调用低层模块,图1是一个这样层次的好例子。因此,一个设计良好的面向对象程序的依赖结构,对应于传统的过程式方法通常会形成的依赖结构,是"倒置"的。
想想高层模块依赖于低层模块的寓意。是高层模块包含一个应用的重要策略决定和商业模式,是这些模块包含应用的身份。然而,当这些模块依赖较低级的模块, 低层模块的改变就对它们有直接的影响,并且迫使他们去改变。
迫迫 是高层模块应该优于低层模块。无论如何,高层模块完全不应该依赖低层模块。
而且,高层模块才是我们想要复用的。 我们已经十分擅长以美国的形式复用低层模块。当高层模块依赖低层模块,在不同的上下文中复用那些高层模块就非常困难。另一方面,当高层模块不依赖低层模块时,高层模块可以被十分简单复用。
正是这个原则,它是框架设计(框架设计。)的核心。
分层(分层)
按照布希”所有良好结构化的面向对象架构(结构良好的面向对象体系结构)都有定义清楚的层次,通过【虽然似乎应该是通过【参考译文】定义良好及受控的接口(接口)使每个层提供某种相关的一些服务"。
图三:简单的分层
这句话的一种幼稚的(天真)解释会导致设计者搞出一个类似于图三的结构。在这张图解里,高层的类政策(策略)使用一个较低层的机制(机制); (后者)依次的使用一个细节的层效用(工具)类。这种情况也许看起来是恰当的,但这里存在一个随时会引爆的地雷(阴险的特点)——政策层对于深入到公用事业层内的所有方式的改变都是敏感的。(因为)依赖是可传递的。政策层依赖某些东西,而某些东西又依赖效用层,故而政策层传递性的依赖效用Ÿ,ŸŸ
图4显示一个更合适的模型。每个较低级别的层由一种抽象类(抽象类)抽抽抽抽抽抽抽抽抽抽抽抽抽抽抽抽抽抽抽抽 因此,每个层都不依赖任何其它层。相反,层依赖抽象类。不仅政策层对效用层的传递性依赖被断开,甚至政策层对机制层的直接依赖也被断开。
使用本模型,机制层或者效用美国政策层,而且,对于所定义的符合机制层接口的低层模块之任何上下文中,政策层都能复用。因此,通过倒置依赖性,我们就构建了兼有更灵活,更耐用,可移植性更强的结构。
C类++中的接口与实现相分离
有人会抱怨道,图三中的结构并不存在我声称的依赖和传递性依赖问题,不管怎么说,政策层仅仅依赖于机制层的接口。机制层的实现(实施)的变化怎么会向上最终影响到政策层呢?
在一些面向对象语言中,这可能是真的。在那些语言中,接口自动的从实现中分离开来。【表扬Java语言一下。】然而,在C类++中,接口与实现并不存在分离。准确的说,C类++中,分离的是类的定义和它的成员函数的定义。
C类++中,我们通常把一个类分为两块,a.小时和交流;.小时文件包含类的定义。.cc(立方厘米)文件包含类的成员函数的定义。类的定义——在.小时文件中,还包含了所有类的所有成员函数和所有成员变量的声明。这种信息超出了简单的接口。类所需要的所有的功能函数(实用程序函数)和.小时文件中声明。这些功能和私有变量是类的实现的一部分,而它们出现在其中的模块是该类的所有用户都必须是依赖的。因此,C类++中,实现并不是自动与接口相分离的。
C类++中,接口与实现相分离的缺欠,能够使用纯粹的抽象的类(纯抽象类)来处理。纯抽象类是这样的类,它除了包含纯虚函数外,一无所有。这样的类才是纯粹的接口,并且它的.小时文件不包含实现。图4展示了这一结构。图4抽抽(类)以使得每个层仅仅依赖于子层的接口。
§一个简单的例子
无论在哪里,一个类向另一个类发送消息时都能应用依赖倒置。例如,考虑一下按钮对象和指示灯对象的案例。
按钮(按钮)对象能感知外部环境。它能测定用户是已经“按”还是没有按它。这种感知的机制是什么并不重要。它可能是图形用户界面上的一个按钮图标,或者被人的指头按的物理按钮,甚至是家用安全系统的运动探测器。按钮(按钮)对象探测用户是激活它还是使其失效(开或关)。指示灯(灯)对象感受(影响)外部环境(的刺激)。一旦接受到打开(打开)的消息,灯就发出某种形式的光。当接受到关闭(关灯)消息,就熄灭该光。其物理机制不重要,它可能是电脑控制台上的一个发光二极管,停车场的汞蒸气灯或是一个激光打印机的激光。
我们怎样设计一个系统使按钮对象控制指示灯[希腊语]?图5展示了一个幼稚的模型。按钮对象简单的发送开启和关闭消息给指示灯对象。方便起见,按钮类使用“包含”的关系来拥有一个指示灯类的实例。列表五显示了这个模型的C类++代码。
图5:幼稚/灯模型
清单5:幼稚的按钮/灯代码
--------------灯.h----------------
类灯{
公众:
void TurnOn();
void TurnOff();
};
-------------按钮.h---------------
类灯;
类按钮{
公众:
按钮(灯&l):其灯(&l){}
无效检测();
私人:
灯*其灯;
};
-------------按钮.cc--------------
#包括“button.h”
#包括“lamp.h”
无效按钮::检测(){
bool buttonOn=获取物理状态();
if(按钮打开)
its灯->打开();
其他的
其灯->关闭();
}
注意:按钮类是直接依赖于指示灯类的。实际上,按钮.cc文件#包括了指示灯。小时文件。这种依赖性暗示,无论指示灯类何时改变,按钮类就必须改变,或者至少要重新编译。而且,复用按钮类来操作马达对象是不可能的。图5和列表5违反了依赖倒置原则。应用的高层(政策)没有与低层模块相分离;抽象没有与细节相分离。没有这样的分离,高层自动的依赖于低层模块,并且抽象也自动的依赖于细节。
发掘潜在的抽象
什么是高水平的策略?正是抽象作为应用的基础,原理(真理)是不随细节的变化而改变。在按钮/灯的例子中,潜在的抽象是识别出用户的开/关信息并将这个信息传递到目标对象。识别用户信号的机制是什么?管它呢!目标物体是什么?管它呢!这些都是不影响抽象的枝末细节。
为了与依赖倒置原则相符,我们必须把这种抽象与问题的细节相隔离。因而我们必须赖,赖。图6显示了这种设计。
图6中国按钮类的抽象从它的详细实现中隔离出来。列表6给出了相应的代码。注意:高层的政策完全封装(捕获)在抽象按钮类中,按钮类对检测用户信号的物理机械作用全然不知;对灯更是一个人:按钮实现和指示灯。
列表6中高层策略能够复用到任何按钮上,以及任何类型的需要控制的设备。而且,它不受低层机制变化的影响。 因此它在变化面前是鲁棒的,灵活的,且可复用的。
----------字节客户端.h---------
类ButtonClient
{
公众:
虚拟void TurnOn()=0;
虚拟void TurnOff()=0;
};
-----------按钮.h---------------
类ButtonClient;
类按钮
{
公众:
按钮(ButtonClient&);
无效检测();
虚拟bool GetState()=0;
私人:
ButtonClient*其客户端;
};
---------按钮.cc----------------
#包含按钮。小时
#包括按钮客户端。小时
按钮::按钮(ButtonClient&bc)
:its客户端(&bc){}
无效按钮::检测()
{
bool buttonOn=获取状态();
if(按钮打开)
其客户端->TurnOn();
其他的
其客户端->TurnOff();
}
-----------灯.h----------------
类灯:公共按钮客户端
{
公众:
虚拟void TurnOn();
虚拟空位关闭();
};
---------按钮Imp.h-------------
类按钮实现
:公共按钮
{
公众:
按钮示例(
ButtonClient&);
虚拟bool GetState();
};
抽象的更进一步扩展
人们(一次?个)能对图/清单6的设计提出正常的埋怨。由按钮控制的设备必须派生于按钮客户端。如果指示灯类来自第三方库,并且我们不能修改源代码该怎么办。
图7:指示灯适配器
图7示范了如何使用适配器模式将第三方指示灯对象连接到模块上的。灯适配器类简单地将从按钮客户端中继承的打开和关闭信息指示灯类需要明白的那些消息。
结论
依赖倒置原理是面向对象技术宣称的很多优越性的根源。对其适当的应用是创造可复用框架所必要的。要想构造出对变化富于弹性的代码,该原理也极其重要。既然抽和彼
这篇文章是我的新书——《模式和面向对象设计高级原则》,很快将由普伦蒂斯·霍尔出版——中一章的高度浓缩版本。在后面的文章中,我们将探索面向对象设计许多其他原理。我们还将会学习各种设计模式(设计模式),以及它们联系到C类++实现时的强大或薄弱之处。我们还将在C类++中国布奇的类理论(类别),它们作为C类++命名空间(名称空间)的适用性。我们还将定义OOD公司中什么是"内聚性(凝聚)"(凝聚)和"耦合"(联轴器),并且我们将会开发出衡量面向对象设计质量的方法学(韵律学)此后,我们将会讨论许多其他感兴趣的论题。
另外的一篇:
|