[06]一些面向对象的设计原则

整理文档很辛苦,赏杯茶钱您下走!

免费阅读已结束,点击下载阅读编辑剩下 ...

阅读已结束,您可以下载文档离线阅读编辑

资源描述

面向对象的设计原则1.开闭原则(OCP)2.里氏代换原则(LSP)3.依赖倒转原则(DIP)4.接口隔离原则(ISP)5.合成聚合复用原则(CARP)6.迪米特法则(LoD)7.单责任原则(SRP)开闭原则(OCP)开闭原则是面向对象程序设计的第一原则,这个原则最早由BertrandMeyer提出:“Softwareentitiesshouldbeopenforextension,butclosedformodification”,即一个软件实体应该对扩展开放,对修改关闭。也就是说,当一个软件需要增加或者修改某些功能时候,应该尽可能的只是在原来的实体中增加代码,而不是修改代码。开闭原则保证了系统具有一定的稳定性,同时保证了系统的灵活性。开闭原则的另外一个叫法称为“对可变性的封装原则”。为什么要OCP?通常,对于开发完的代码都需要多种测试才能够投入使用,这包括:1设计人员进行初期的架构设计2要经过开发人员的单元测试、集成测试。3然后再到测试人员的白盒测试、黑盒测试。4最后还要由用户进行一定的测试。经过漫长的测试,代码才能够投入使用。但是软件产品的维护和升级又是一个永恒的话题,在维护的过程中,你可能要不断地增加一些小功能;在升级的过程中,你要增加一些较大的功能。这种功能的扩展,就要求我们改变原有的代码。但是,对原代码的修改就会深刻地影响到原来的功能的方方面面:1可能对旧代码引入了新的错误,使你不得不对旧代码进行大规模的修改。2可能引起你不得不重新构造系统的架构。3即使新增的代码对旧代码没有影响,你也不得不对原来的系统做一个全面的测试。4经过一段时间,也许你认为以前代码更好,更符合用户需求所有上述列出来的问题,都是对系统功能进行扩展所不能承受的代价。换句话说,我们设计出来的系统,一定要是扩展性良好的系统。如何才能够设计出扩展性良好的系统呢?这就需要在软件系统设计时遵守开闭原则若财务部颁布主板和内存应使用额外费用,则需修改如下:开闭原则(OCP)计算部件数组中各个部件价格的总和。开闭原则(OCP)较好的一种设计方式是:将计价策略合并到Part的getPrice()方法中。Part和ConcretePart类的示例如下:如何在OO中引入OCP原则?把对实体的依赖改为对抽象的依赖就行了。下面的例子说明了这个过程:05赛季的时候,一辆F1赛车有一台V10引擎。但是到了06赛季,国际汽联修改了规则,一辆F1赛车只能安装一台V8引擎。车队很快投入了新赛车的研发,不幸的是,从工程师那里得到消息,旧车身的设计不能够装进新研发的引擎。我们不得不为新的引擎重新打造车身,于是一辆新的赛车诞生了。但是,麻烦的事接踵而来,国际汽联频频修改规则,搞得设计师在“赛车”上改了又改,最终变得不成样子,只能把它废弃。为了能够重用这辆昂贵的赛车,工程师们提出了解决方案:首先,在车身的设计上预留出安装引擎的位置和管线。然后,根据这些设计好的规范设计引擎(或是引擎的适配器)。于是,新的赛车设计方案就这样诞生了。做到开闭原则,就注意以下两点。1)多使用抽象类在设计类时,对于拥有共同功能的相似类进行抽象化处理,将公用的功能部分放到抽象类中,所有的操作都调用子类。这样,在需要对系统进行功能扩展时,只需要依据抽象类实现新的子类即可。如图10-1所示,在扩展子类时,不仅可以拥有抽象类的共有属性和共有函数,还可以拥有自定义的属性和函数。2)多使用接口与抽象类不同,接口只定义子类应该实现的接口函数,而不实现公有的功能。在现在大多数的软件开发中,都会为实现类定义接口,这样在扩展子类时实现该接口。如果要改换原有的实现,只需要改换一个实现类即可。如图各子类由接口类定义了接口函数,只需要在不同的子类中编写不同的实现即可,当然也可以实现自有的函数。面向对象的设计原则1.开闭原则(OCP)2.里氏代换原则(LSP)3.依赖倒转原则(DIP)4.接口隔离原则(ISP)5.合成聚合复用原则(CARP)6.迪米特法则(LoD)7.单责任原则(SRP)里氏代换原则(LSP)里氏代换原则最早由BarbaraLiskov提出:“基类出现的地方,子类一定可以出现”,也就是对基类是合法的操作,对子类也一定合法。子类型必须能够替换掉它们的父类型、并出现在父类能够出现的任何地方。并且经过替换以后,代码还能正常工作。正方形是否是长方形的子类?第一个例子:正方形不是长方形“正方形不是长方形”是一个理解里氏代换原则的最经典的例子。在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统中,让正方形继承自长方形是顺利成章的事情。现在,我们截取该系统的一个代码片段进行分析:第一个例子:正方形不是长方形长方形类Rectangle:classRectangle{doublelength;doublewidth;public:doublegetLength(){returnlength;}voidsetLength(doubleheight){this.length=length;}doublegetWidth(){returnwidth;}voidsetWidth(doublewidth){this.width=width;}}正方形类Square:classSquare:publicRectangle{public:voidsetWidth(doublewidth){Rectangle::setLength(width);Rectangle::setWidth(width);}voidsetLength(doublelength){Rectangle::.setLength(length);Rectangle::.setWidth(length);}}第一个例子:正方形不是长方形由于正方形的度和宽度必须相等,所以在方法setLength和setWidth中,对长度和宽度赋值相同。类TestRectangle是我们的软件系统中的一个组件,它有一个resize方法要用到基类Rectangle,resize方法的功能是模拟长方形宽度逐步增长的效果:测试类TestRectangle:classTestRectangle{public:voidresize(Rectangle&objRect){while(objRect.getWidth()=objRect.getLength()){objRect.setWidth(objRect.getWidth()+1);}}}第一个例子:正方形不是长方形我们运行一下这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。我们得出结论:在resize方法中,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。第二个例子:鸵鸟不是鸟“鸵鸟非鸟”也是一个理解里氏代换原则的经典的例子。“鸵鸟非鸟”的另一个版本是“企鹅非鸟”,这两种说法本质上没有区别,前提条件都是这种鸟不会飞。生物学中对于鸟类的定义:“恒温动物,卵生,全身披有羽毛,身体呈流线形,有角质的喙,眼在头的两侧。前肢退化成翼,后肢有鳞状外皮,有四趾”。所以,从生物学角度来看,鸵鸟肯定是一种鸟。我们设计一个与鸟有关的系统,鸵鸟类顺理成章地由鸟类派生,鸟类所有的特性和行为都被鸵鸟类继承。大多数的鸟类在人们的印象中都是会飞的,所以,我们给鸟类设计了一个名字为fly的方法,还给出了与飞行相关的一些属性,比如飞行速度(velocity)。鸟类Bird:classBird{doublevelocity;public:voidfly(){//Iamflying;};voidsetVelocity(doublevelocity){this.velocity=velocity;};doublegetVelocity(){returnthis.velocity;};}鸵鸟不会飞怎么办?我们就让它扇扇翅膀表示一下吧,在fly方法里什么都不做。至于它的飞行速度,不会飞就只能设定为0了,于是我们就有了鸵鸟类的设计。鸵鸟类Ostrich:classOstrich:publicBird{publicfly(){//Idonothing;};publicsetVelocity(doublevelocity){this.velocity=0;};publicgetVelocity(){return0;};}鸵鸟不是鸟好了,所有的类都设计完成,我们把类Bird提供给了其它的代码(消费者)使用。现在,消费者使用Bird类完成这样一个需求:计算鸟飞越黄河所需的时间。对于Bird类的消费者而言,它只看到了Bird类中有fly和getVelocity两个方法,至于里面的实现细节,它不关心,而且也无需关心,于是给出了实现代码:测试类TestBird:classTestBird{public:voidcalcFlyTime(Birdbird){try{doubleriverWidth=3000;cout.riverWidth/bird.getVelocity()endl;}catch(…){coutAnerroroccured!endl;}};}第二个例子:鸵鸟不是鸟如果我们拿一种飞鸟来测试这段代码,没有问题,结果正确,符合我们的预期,系统输出了飞鸟飞越黄河的所需要的时间;如果我们再拿鸵鸟来测试这段代码,结果代码发生了系统除零的异常,明显不符合我们的预期。对于TestBird类而言,它只是Bird类的一个消费者,它在使用Bird类的时候,只需要根据Bird类提供的方法进行相应的使用,根本不会关心鸵鸟会不会飞这样的问题,而且也无须知道。它就是要按照“所需时间=黄河的宽度/鸟的飞行速度”的规则来计算鸟飞越黄河所需要的时间。我们得出结论:在calcFlyTime方法中,Bird类型的参数是不能被Ostrich类型的参数所代替,如果进行了替换就得不到预期结果。因此,Ostrich类和Bird类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,鸵鸟不是鸟。原因分析1:对类的继承关系的定义没有搞清楚面向对象的设计关注的是对象的行为,它是使用“行为”来对对象进行分类的,只有行为一致的对象才能抽象出一个类来。类的继承关系就是一种“Is-A”关系,实际上指的是行为上的“Is-A”关系,可以把它描述为“Act-As”。我们再来看“正方形不是长方形”这个例子,正方形在设置长度和宽度这两个行为上,与长方形显然是不同的。长方形的行为:设置长方形的长度的时候,它的宽度保持不变,设置宽度的时候,长度保持不变。正方形的行为:设置正方形的长度的时候,宽度随之改变;设置宽度的时候,长度随之改变。所以,如果我们把这种行为加到基类长方形的时候,就导致了正方形无法继承这种行为。我们“强行”把正方形从长方形继承过来,就造成无法达到预期的结果。“鸵鸟非鸟”基本上也是同样的道理。我们一讲到鸟,就认为它能飞,有的鸟确实能飞,但不是所有的鸟都能飞。问题就是出在这里。如果以“飞”的行为作为衡量“鸟”的标准的话,鸵鸟显然不是鸟;如果按照生物学的划分标准:有翅膀、有羽毛等特性作为衡量“鸟”的标准的话,鸵鸟理所当然就是鸟了。鸵鸟没有“飞”的行为,我们强行给它加上了这个行为,所以在面对“飞越黄河”的需求时,代码就会出现运行期故障。原因分析2:设计要依赖于用户要求和具体环境。继承关系要求子类要具有基类全部的行为。这里的行为是指落在需求范围内的行为。图中鸟类具有4个对外的行

1 / 44
下载文档,编辑使用

©2015-2020 m.777doc.com 三七文档.

备案号:鲁ICP备2024069028号-1 客服联系 QQ:2149211541

×
保存成功