- 83.50 KB
- 6页
- 1、本文档共5页,可阅读全部内容。
- 2、本文档内容版权归属内容提供方,所产生的收益全部归内容提供方所有。如果您对本文有版权争议,可选择认领,认领后既往收益都归您。
- 3、本文档由用户上传,本站不保证质量和数量令人满意,可能有诸多瑕疵,付费之前,请仔细先通过免费阅读内容等途径辨别内容交易风险。如存在严重挂羊头卖狗肉之情形,可联系本站下载客服投诉处理。
- 文档侵权举报电话:19940600175。
'OOD设计基本原则ØOCP原则Ø里氏替换原则Ø依赖倒置原则Ø接口隔离原则Ø聚合与继承原则Ø单一职责原则ØSeparationofconcernsPrincipleØParetoPrinciple(帕雷多原则80/20原则)OOD设计原则在提高一个系统可维护性的同时,提高这个系统的可复用性.他们是一些指导原则,依照这些原则设计,我们就可以有效的提高系统的复用性,同时提高系统的可维护性.OCP原则Open-ClosedPrinciple这些OOD原则的一个基石就是"开-闭原则"(Open-ClosedPrincipleOCP).这个原则最早是由BertrandMeyer提出,英文的原文是:Softwareentitiesshouldbeopenforextension,butclosedformodification.意思是说,一个软件实体应当对扩展开放,对修改关闭.也就是说,我们在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,换句话说就是,应当可以在不必修改源代码的情况下改变这个模块的行为.满足OCP的设计给系统带来两个无可比拟的优越性.Ø通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新需求,使变化中的软件系统有一定的适应性和灵活性.Ø已有的软件模块,特别是最重要的抽象层模块不能再修改,这就使变化中的软件系统有一定的稳定性和延续性.具有这两个优点的软件系统是一个高层次上实现了复用的系统,也是一个易于维护的系统.那么,我们如何才能做到这个原则呢?不能修改而可以扩展,这个看起来是自相矛盾的.其实这个是可以做到的,按面向对象的说法,这个就是不允许更改系统的抽象层,而允许扩展的是系统的实现层.解决问题的关键在:抽象化.我们让模块依赖于一个固定的抽象体,这样它就是不可以修改的;同时,通过这个抽象体派生,我们就可以扩展此模块的行为功能.如此,这样设计的程序只通过增加代码来变化而不是通过更改现有代码来变化,前面提到的修改的副作用就没有了."开-闭"原则如果从另外一个角度讲述,就是所谓的"对可变性封装原则"(PrincipleofEncapsulationofVariation,EVP).讲的是找到一个系统的可变因素,将之封装起来.在我们考虑一个系统的时候,我们不要把关注的焦点放在什么会导致设计发生变化上,而是考虑允许什么发生变化而不让这一变化导致重新设计.也就是说,我们要积极的面对变化,积极的包容变化,而不是逃避. [SHALL01]将这一思想用一句话总结为:"找到一个系统的可变因素,将它封装起来",并将它命名为"对可变性的封装原则"."对可变性的封装原则"意味者两点:Ø一种可变性应当被封装到一个对象里面,而不应当散落到代码的很多角落里面.同一种可变性的不同表象意味着同一个继承等级结构中的具体子类.继承应当被看做是封装变化的方法,而不应当是被认为从一般的对象生成特殊的对象的方法(继承经常被滥用).Ø一种可变性不应当与另外一种可变性混合在一起.从具体的类图来看,如果继承结构超过了两层,那么就意味着将两种不同的可变性混合在了一起.
"对可变性的封装原则"从工程的角度说明了如何实现OCP.如果按照这个原则来设计,那么系统就应当是遵守OCP的.但是现实往往是残酷的,我们不可能100%的遵守OCP,但是我们要向这个目标来靠近.设计者要对设计的模块对何种变化封闭做出选择.里氏替换原则LiskovSubstitutionPrinciple从上一篇的"开-闭"原则中可以看出,面向对象设计的重要原则是创建抽象化,并且从抽象化导出具体化.这个导出要使用继承关系和一个原则:里氏替换原则(LiskovSubstitutionPrinciple,LSP). 那么什么是里氏替换原则呢?有个严格的表述,绕口,不好记.还是比较白话的这个好记.说的是:一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它察觉不出基类对象和子类对象的区别.也就是说,在软件里面,把基类都替换成它的子类,程序的行为没有变化. LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为. 下面,我们从代码重构的角度来对LSP进行理解.LSP讲的是基类和子类的关系.只有当这种关系存在时,里氏替换关系才存在.如果两个具体的类A,B之间的关系违反了LSP的设计,(假设是从B到A的继承关系)那么根据具体的情况可以在下面的两种重构方案中选择一种. Ø创建一个新的抽象类C,作为两个具体类的超类,将A,B的共同行为移动到C中来解决问题.从B到A的继承关系改为委派关系.为了说明,我们先用第一种方法来看一个例子。第二种办法在另外一个原则中说明.我们就看那个著名的长方形和正方形的例子.对于长方形的类,如果它的长宽相等,那么它就是一个正方形,因此,长方形类的对象中有一些正方形的对象.对于一个正方形的类,它的方法有个setSide和getSide,它不是长方形的子类,和长方形也不会符合LSP. 那么,如果让正方形当做是长方形的子类,会出现什么情况呢?我们让正方形从长方形继承,然后在它的内部设置width等于height,这样,只要width或者height被赋值,那么width和height会被同时赋值,这样就保证了正方形类中,width和height总是相等的.现在我们假设有个客户类,其中有个方法,规则是这样的,测试传人的长方形的宽度是否大于高度,如果满足就停止下来,否则就增加宽度的值.现在我们来看,如果传人的是基类长方形,这个运行的很好.根据LSP,我们把基类替换成它的子类,结果应该也是一样的,但是因为正方形类的width和height会同时赋值,这个方法没有结束的时候,条件总是不满足,也就是说,替换成子类后,程序的行为发生了变化,它不满足LSP. 那么我们用第一种方案进行重构,我们构造一个抽象的四边形类,把长方形和正方形共同的行为放到这个四边形类里面,让长方形和正方形都是它的子类,问题就OK了.对于长方形和正方形,取width和height是它们共同的行为,但是给width和height赋值,两者行为不同,因此,这个抽象的四边形的类只有取值方法,没有赋值方法.上面的例子中那个方法只会适用于不同的子类,LSP也就不会被破坏. 在进行设计的时候,我们尽量从抽象类继承,而不是从具体类继承.如果从继承等级树来看,所有叶子节点应当是具体类,而所有的树枝节点应当是抽象类或者接口.当然这个只是一个一般性的指导原则,使用的时候还要具体情况具体分析.依赖倒置原则Dependency-InversionPrinciples
"开-闭"原则是我们OOD的目标,达到这一目标的主要机制就是"依赖倒转原则".这个原则的内容是:要依赖于抽象,不要依赖于具体.对于抽象层次来说,它是一个系统的本质的概括,是系统的商务逻辑和宏观的,战略性的决定,是必然性的体现;具体的层次则是与实现有关的算法和逻辑,一些战术性的决定,带有相当大的偶然性.传统的过程性系统设计办法倾向于使高层次的模块依赖于低层次的模块;抽象层次依赖于具体层次.这实际上就是微观决定宏观,战术决定战略,偶然决定必然.依赖倒转原则就是要把这种错误的依赖关系倒转过来.许多的建构设计模型,例如COM,CORBA,JavaBean,EJB等,它们背后的基本原则就是DIP.对于软件设计的两个目标,复用和可维护性来说,传统的设计侧重于具体层次模块的复用和可维护,比如算法,数据结构,函数库等等.但是,对系统的抽象是比较稳定的,它的复用是很重要的,同时,抽象层次的可维护性也应当是一个重点.就是说DIP也导致复用和可维护性的"倒转".我们现在来看看依赖有几种,依赖也就是耦合,分为下面三种Ø零耦合(NilCoupling)关系,两个类没有依赖关系,那就是零耦合Ø具体耦合(ConcreteCoupling)关系,两个具体的类之间有依赖关系,那么就是具体耦合关系,如果一个具体类直接引用另外一个具体类,就会发生这种关系.Ø抽象耦合(AbstractCoupling)关系.这种关系发生在一个具体类和一个抽象类之间,这样就使必须发生关系的类之间保持最大的灵活性.DIP要求客户端依赖于抽象耦合,抽象不应当依赖于细节,细节应当依赖于抽象(Abstractionsshouldnotdependupondetails.Detailsshoulddependuponabstractions),这个原则的另外一个表述就是"四人团"强调的那个:要针对接口编程,不要对实现编程.(Programtoaninterface,notanimplementation),程序在需要引用一个对象时,应当尽可能的使用抽象类型作为变量的静态类型,这就是针对接口编程的含义.DIP是达到"开-闭"原则的途径.要做到DIP,用抽象方式耦合是关键.由于一个抽象耦合总要涉及具体类从抽象类继承.并且需要保证在任何引用到某类的地方都可以改换成其子类,因此,LSP是DIP的基础.DIP是OOD的核心原则,设计模式的研究和应用都是用它作为指导原则的.DIP虽然强大,但是也很难实现.另外,DIP是假定所有的具体类都会变化,这也不是全对,有些具体类就相当稳定.使用这个类的客户端就完全可以依赖这个具体类而不用再弄一个抽象类.接口隔离原则InterfaceSegregationPrinciple接口隔离原则(ISP):使用多个专门的接口比使用单一的总接口要好.也就是说,一个类对另外一个类的依赖性应当是建立在最小的接口上的.这里的"接口"往往有两种不同的含义:Ø一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象;Ø另外一种是指某种语言具体的"接口"定义,有严格的定义和结构.比如Java语言里面的Interface结构.对于这两种不同的含义,ISP的表达方式以及含义都有所不同.(上面说的一个类型,可以理解成一个类,我们定义了一个类,也就是定义了一种新的类型)当我们把"接口"理解成一个类所提供的所有方法的特征集合的时候,这就是一种逻辑上的概念.接口的划分就直接带来类型的划分.这里,我们可以把接口理解成角色,一个接口就只是代表一个角色,每个角色都有它特定的一个接口,这里的这个原则可以叫做"角色隔离原则".
如果把"接口"理解成狭义的特定语言的接口,那么ISP表达的意思是说,对不同的客户端,同一个角色提供宽窄不同的接口,也就是定制服务,个性化服务.就是仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来.在我们进行OOD的时候,一个重要的工作就是恰当的划分角色和角色对应的接口.将没有关系的接口合并在一起,是对角色和接口的污染.如果将一些看上去差不多的接口合并,并认为这是一种代码优化,这是错误的.不同的角色应该交给不同的接口,而不能都交给一个接口.对于定制服务,这样做最大的好处就是系统的可维护性.向客户端提供接口是一种承诺,public接口后是不能改变的,因此不必要的承诺就不要做出,承诺越少越好.聚合与继承原则合成(Composition)和聚合(Aggregation)都是关联(Association)的特殊种类。聚合表示整体和部分的关系,表示“拥有”;合成则是一种更强的“拥有”,部分和整体的生命周期一样。合成的新的对象完全支配其组成部分,包括它们的创建和湮灭等。一个合成关系的成分对象是不能与另一个合成关系共享的。换句话说,合成是值的聚合(AggregationbyValue),而一般说的聚合是引用的聚合(AggregationbyReference)。简短的说,合成-聚合复用原则(CARP)是指,尽量使用合成/聚合,而不是使用继承。在OOD中,有两种基本的办法可以实现复用,一种是通过合成/聚合,另外一种就是通过继承。通过合成/聚合的好处是:Ø新对象存取成分对象的唯一方法是通过成分对象的接口。Ø这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。Ø这种复用支持包装。Ø这种复用所需的依赖较少。Ø每一个新的类可以将焦点集中在一个任务上。Ø这种复用可以在运行时间内动态进行,新对象可以动态的引用与成分对象类型相同的对象。Ø作为复用手段可以应用到几乎任何环境中去。Ø它的缺点就是系统中会有较多的对象需要管理。 通过继承来进行复用的优点是:Ø新的实现较为容易,因为超类的大部分功能可以通过继承的关系自动进入子类。Ø修改和扩展继承而来的实现较为容易。缺点是:Ø继承复用破坏封装,因为继承将超类的实现细节暴露给子类。由于超类的内部细节常常是对于子类透明的,所以这种复用是透明的复用,又称“白箱”复用。Ø如果超类发生改变,那么子类的实现也不得不发生改变。Ø从超类继承而来的实现是静态的,不可能在运行时间内发生改变,没有足够的灵活性。Ø继承只能在有限的环境中使用。如何选择?要正确的选择合成/复用和继承,必须透彻的理解里氏替换原则和Coad法则。里氏替换原则前面学习过,Coad法则由PeterCoad提出,总结了一些什么时候使用继承作为复用工具的条件。只有当以下的Coad条件全部被满足时,才应当使用继承关系:Ø子类是超类的一个特殊种类,而不是超类的一个角色,也就是区分“Has-A”和“Is-A”。只有“Is-A”关系才符合继承关系,“Has-A”关系应当用聚合来描述。Ø永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变成另外一个子类的话,就不要使用继承。
Ø子类具有扩展超类的责任,而不是具有置换调(override)或注销掉(Nullify)超类的责任。如果一个子类需要大量的置换掉超类的行为,那么这个类就不应该是这个超类的子类。Ø只有在分类学角度上有意义时,才可以使用继承。不要从工具类继承。错误的使用继承而不是合成/聚合的一个常见原因是错误的把“Has-A”当成了“Is-A”。“Is-A”代表一个类是另外一个类的一种;“Has-A”代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。我们看一个例子。如果我们把“人”当成一个类,然后把“雇员”,“经理”,“学生”当成是“人”的子类。这个的错误在于把“角色”的等级结构和“人”的等级结构混淆了。“经理”,“雇员”,“学生”是一个人的角色,一个人可以同时拥有上述角色。如果按继承来设计,那么如果一个人是雇员的话,就不可能是经理,也不可能是学生,这显然不合理。正确的设计是有个抽象类“角色”,“人”可以拥有多个“角色”(聚合),“雇员”,“经理”,“学生”是“角色”的子类。另外一个就是只有两个类满足里氏替换原则的时候,才可能是“Is-A”关系。也就是说,如果两个类是“Has-A”关系,但是设计成了继承,那么肯定违反里氏替换原则。单一职责原则SingleResponsibilityPrinciple(SRP)就一个类而言,应该只专注于做一件事和仅有一个引起它变化的原因。所谓职责,我们可以理解他为功能,就是设计的这个类功能应该只有一个,而不是两个或更多。也可以理解为引用变化的原因,当你发现有两个变化会要求我们修改这个类,那么你就要考虑撤分这个类了。因为职责是变化的一个轴线,当需求变化时,该变化会反映类的职责的变化。“就像一个人身兼数职,而这些事情相互关联不大,,甚至有冲突,那他就无法很好的解决这些职责,应该分到不同的人身上去做才对。”二、举例说明:违反SRP原则代码:modem接口明显具有两个职责:连接管理和数据通讯;interfaceModem{publicvoiddial(stringpno);publicvoidhangup();publicvoidsend(charc);publicvoidrecv();}如果应用程序变化影响连接函数,那么就需要重构:interfaceDataChannel{publicvoidsend(charc);publicvoidrecv();}interfaceConnection{publicvoiddial(stringpno);publicvoidhangup();}三、SRP优点:
消除耦合,减小因需求变化引起代码僵化性臭味四、使用SRP注意点:1、一个合理的类,应该仅有一个引起它变化的原因,即单一职责;2、在没有变化征兆的情况下应用SRP或其他原则是不明智的;3、在需求实际发生变化时就应该应用SRP等原则来重构代码;4、使用测试驱动开发会迫使我们在设计出现臭味之前分离不合理代码;5、如果测试不能迫使职责分离,僵化性和脆弱性的臭味会变得很强烈,那就应该用Facade或Proxy模式对代码重构;SeparationofconcernsPrincipleIncomputerscience,separationofconcerns(SoC)istheprocessofbreakingacomputerprogramintodistinctfeaturesthatoverlapinfunctionalityaslittleaspossible.Aconcernisanypieceofinterestorfocusinaprogram.Typically,concernsaresynonymouswithfeaturesorbehaviors.ProgresstowardsSoCistraditionallyachievedthroughmodularityandencapsulation,withthehelpofinformationhiding.Layereddesignsininformationsystemsarealsooftenbasedonseparationofconcerns(e.g.,presentationlayer,businesslogiclayer,dataaccesslayer,databaselayer).AllprogrammingparadigmsaiddevelopersintheprocessofimprovingSoC.Forexample,object-orientedprogramminglanguagessuchasC++,Java,andC#canseparateconcernsintoobjects,andadesignpatternlikeMVCcanseparatecontentfrompresentationanddata-processing(model)fromcontent.Service-orienteddesigncanseparateconcernsintoservices.ProceduralprogramminglanguagessuchasCandPascalcanseparateconcernsintoprocedures.Aspect-orientedprogramminglanguagescanseparateconcernsintoaspectsandobjects.Separationofconcernsisanimportantdesignprincipleinmanyotherareasaswell,suchasurbanplanning,architectureandinformationdesign.Thegoalistodesignsystemssothatfunctionscanbeoptimizedindependentlyofotherfunctions,sothatfailureofonefunctiondoesnotcauseotherfunctionstofail,andingeneraltomakeiteasiertounderstand,designandmanagecomplexinterdependentsystems.Commonexamplesincludeusingcorridorstoconnectroomsratherthanhavingroomsopendirectlyintoeachother,andkeepingthestoveononecircuitandthelightsonanother.ParetoPrinciple(帕雷多原则80/20原则)1960年意大利经济学家帕雷多建立了一个数学模型来描述国家不平等的财富分配,发现20%的人拥有了财富的80%。在帕雷多经过观察并建立了模型之后,许多人都在他们各自的领域发现了同样的现象。关于“重要的少数和普遍的多数”的发现,即20%因素往往决定事物80%的结果变成了有名的帕雷多定律或者80/20原则。80/20原则的含义是一切事物都是这样组成的:20%是至关重要的,而80%是平常的。80/20原则在软件开发领域的体现:1)软件开发中20%的功能是用户经常使用的,80%的功能其实没有那么重要;2)软件框架的设计能满足80%的应用开发,20%可能并不适用;在架构设计中要意思到不能满足所有的情况,同时考虑特殊情况的特殊处理;3)软件开发团队中非核心成员和核心成员也体现了80/20原则;4)80%的软件Bug集中在20%的软件模块中,体现了重点核心模块的重点开发和维护。 '