还剩4页未读,继续阅读
文本内容:
OOD设计基本原则OCP原则里氏替换原则依赖倒置原则接口隔离原则聚合与继承原则单一职责原则SeparationofconcernsPrincipleParetoPrinciple帕雷多原则80/20原则OOD设计原则在提高一个系统可维护性的同时提高这个系统的可复用性.他们是一些指导原则依照这些原则设计我们就可以有效的提高系统的复用性同时提高系统的可维护性.OCP原则Open-ClosedPrinciple这些OOD原则的一个基石就是开-闭原则Open-ClosedPrincipleOCP.这个原则最早是由BertrandMeyer提出英文的原文是:Softwareentitiesshouldbeopenforextensionbutclosedformodification.意思是说一个软件实体应当对扩展开放对修改关闭.也就是说我们在设计一个模块的时候应当使这个模块可以在不被修改的前提下被扩展换句话说就是应当可以在不必修改源代码的情况下改变这个模块的行为.满足OCP的设计给系统带来两个无可比拟的优越性.通过扩展已有的软件系统可以提供新的行为以满足对软件的新需求使变化中的软件系统有一定的适应性和灵活性.已有的软件模块特别是最重要的抽象层模块不能再修改这就使变化中的软件系统有一定的稳定性和延续性.具有这两个优点的软件系统是一个高层次上实现了复用的系统也是一个易于维护的系统.那么我们如何才能做到这个原则呢不能修改而可以扩展这个看起来是自相矛盾的.其实这个是可以做到的按面向对象的说法这个就是不允许更改系统的抽象层而允许扩展的是系统的实现层.解决问题的关键在:抽象化.我们让模块依赖于一个固定的抽象体这样它就是不可以修改的;同时通过这个抽象体派生我们就可以扩展此模块的行为功能.如此这样设计的程序只通过增加代码来变化而不是通过更改现有代码来变化前面提到的修改的副作用就没有了.开-闭原则如果从另外一个角度讲述就是所谓的对可变性封装原则PrincipleofEncapsulationofVariationEVP.讲的是找到一个系统的可变因素将之封装起来.在我们考虑一个系统的时候我们不要把关注的焦点放在什么会导致设计发生变化上而是考虑允许什么发生变化而不让这一变化导致重新设计.也就是说我们要积极的面对变化积极的包容变化而不是逃避. [SHALL01]将这一思想用一句话总结为:找到一个系统的可变因素将它封装起来并将它命名为对可变性的封装原则.对可变性的封装原则意味者两点:一种可变性应当被封装到一个对象里面而不应当散落到代码的很多角落里面.同一种可变性的不同表象意味着同一个继承等级结构中的具体子类.继承应当被看做是封装变化的方法而不应当是被认为从一般的对象生成特殊的对象的方法继承经常被滥用.一种可变性不应当与另外一种可变性混合在一起.从具体的类图来看如果继承结构超过了两层那么就意味着将两种不同的可变性混合在了一起.对可变性的封装原则从工程的角度说明了如何实现OCP.如果按照这个原则来设计那么系统就应当是遵守OCP的.但是现实往往是残酷的我们不可能100%的遵守OCP但是我们要向这个目标来靠近.设计者要对设计的模块对何种变化封闭做出选择.里氏替换原则LiskovSubstitutionPrinciple从上一篇的开-闭原则中可以看出面向对象设计的重要原则是创建抽象化并且从抽象化导出具体化.这个导出要使用继承关系和一个原则:里氏替换原则LiskovSubstitutionPrincipleLSP. 那么什么是里氏替换原则呢有个严格的表述绕口不好记.还是比较白话的这个好记.说的是:一个软件实体如果使用的是一个基类的话那么一定适用于其子类而且它察觉不出基类对象和子类对象的区别.也就是说在软件里面把基类都替换成它的子类程序的行为没有变化. LSP是继承复用的基石只有当衍生类可以替换掉基类软件单位的功能不受到影响时基类才能真正被复用而衍生类也能够在基类的基础上增加新的行为. 下面我们从代码重构的角度来对LSP进行理解.LSP讲的是基类和子类的关系.只有当这种关系存在时里氏替换关系才存在.如果两个具体的类AB之间的关系违反了LSP的设计假设是从B到A的继承关系那么根据具体的情况可以在下面的两种重构方案中选择一种. 创建一个新的抽象类C作为两个具体类的超类将AB的共同行为移动到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的目标达到这一目标的主要机制就是依赖倒转原则.这个原则的内容是:要依赖于抽象不要依赖于具体.对于抽象层次来说它是一个系统的本质的概括是系统的商务逻辑和宏观的战略性的决定是必然性的体现;具体的层次则是与实现有关的算法和逻辑一些战术性的决定带有相当大的偶然性.传统的过程性系统设计办法倾向于使高层次的模块依赖于低层次的模块;抽象层次依赖于具体层次.这实际上就是微观决定宏观战术决定战略偶然决定必然.依赖倒转原则就是要把这种错误的依赖关系倒转过来.许多的建构设计模型例如COMCORBAJavaBeanEJB等它们背后的基本原则就是DIP.对于软件设计的两个目标复用和可维护性来说传统的设计侧重于具体层次模块的复用和可维护比如算法数据结构函数库等等.但是对系统的抽象是比较稳定的它的复用是很重要的同时抽象层次的可维护性也应当是一个重点.就是说DIP也导致复用和可维护性的倒转.我们现在来看看依赖有几种依赖也就是耦合分为下面三种零耦合NilCoupling关系两个类没有依赖关系那就是零耦合具体耦合ConcreteCoupling关系两个具体的类之间有依赖关系那么就是具体耦合关系如果一个具体类直接引用另外一个具体类就会发生这种关系.抽象耦合AbstractCoupling关系.这种关系发生在一个具体类和一个抽象类之间这样就使必须发生关系的类之间保持最大的灵活性.DIP要求客户端依赖于抽象耦合抽象不应当依赖于细节细节应当依赖于抽象Abstractionsshouldnotdependupondetails.Detailsshoulddependuponabstractions这个原则的另外一个表述就是四人团强调的那个:要针对接口编程不要对实现编程.Programtoaninterfacenotanimplementation程序在需要引用一个对象时应当尽可能的使用抽象类型作为变量的静态类型这就是针对接口编程的含义.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”关系,但是设计成了继承,那么肯定违反里氏替换原则单一职责原则SingleResponsibilityPrincipleSRP就一个类而言,应该只专注于做一件事和仅有一个引起它变化的原因所谓职责,我们可以理解他为功能,就是设计的这个类功能应该只有一个,而不是两个或更多也可以理解为引用变化的原因,当你发现有两个变化会要求我们修改这个类,那么你就要考虑撤分这个类了因为职责是变化的一个轴线,当需求变化时,该变化会反映类的职责的变化“就像一个人身兼数职,而这些事情相互关联不大,,甚至有冲突,那他就无法很好的解决这些职责,应该分到不同的人身上去做才对”
二、举例说明违反SRP原则代码:modem接口明显具有两个职责连接管理和数据通讯;interfaceModem{publicvoiddialstringpno;publicvoidhangup;publicvoidsendcharc;publicvoidrecv;}如果应用程序变化影响连接函数,那么就需要重构interfaceDataChannel{publicvoidsendcharc;publicvoidrecv;}interfaceConnection{publicvoiddialstringpno;publicvoidhangup;}
三、SRP优点消除耦合,减小因需求变化引起代码僵化性臭味
四、使用SRP注意点
1、一个合理的类,应该仅有一个引起它变化的原因,即单一职责;
2、在没有变化征兆的情况下应用SRP或其他原则是不明智的;
3、在需求实际发生变化时就应该应用SRP等原则来重构代码;
4、使用测试驱动开发会迫使我们在设计出现臭味之前分离不合理代码;
5、如果测试不能迫使职责分离,僵化性和脆弱性的臭味会变得很强烈,那就应该用Facade或Proxy模式对代码重构;SeparationofconcernsPrincipleIncomputerscienceseparationofconcernsSoCistheprocessofbreakingacomputerprogramintodistinctfeaturesthatoverlapinfunctionalityaslittleaspossible.Aconcernisanypieceofinterestorfocusinaprogram.Typicallyconcernsaresynonymouswithfeaturesorbehaviors.ProgresstowardsSoCistraditionallyachievedthroughmodularityandencapsulationwiththehelpofinformationhiding.Layereddesignsininformationsystemsarealsooftenbasedonseparationofconcernse.g.presentationlayerbusinesslogiclayerdataaccesslayerdatabaselayer.AllprogrammingparadigmsaiddevelopersintheprocessofimprovingSoC.Forexampleobject-orientedprogramminglanguagessuchasC++JavaandC#canseparateconcernsintoobjectsandadesignpatternlikeMVCcanseparatecontentfrompresentationanddata-processingmodelfromcontent.Service-orienteddesigncanseparateconcernsintoservices.ProceduralprogramminglanguagessuchasCandPascalcanseparateconcernsintoprocedures.Aspect-orientedprogramminglanguagescanseparateconcernsintoaspectsandobjects.Separationofconcernsisanimportantdesignprincipleinmanyotherareasaswellsuchasurbanplanningarchitectureandinformationdesign.Thegoalistodesignsystemssothatfunctionscanbeoptimizedindependentlyofotherfunctionssothatfailureofonefunctiondoesnotcauseotherfunctionstofailandingeneraltomakeiteasiertounderstanddesignandmanagecomplexinterdependentsystems.Commonexamplesincludeusingcorridorstoconnectroomsratherthanhavingroomsopendirectlyintoeachotherandkeepingthestoveononecircuitandthelightsonanother.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%的软件模块中,体现了重点核心模块的重点开发和维护 。