设计原则总结
###单一职责原则(Single Responsibility Principle)
There should never be more than one reason for a class to change.
永远不要让一个类存在多个改变的理由。
换句话说,如果一个类需要改变,改变它的理由永远只有一个。如果存在多个改变它的理由,就需要重新设计该类。
单一职责原则的核心含意是:只能让一个类有且仅有一个职责。这也是单一职责原则的命名含义。
####为什么一个类不能有多于一个以上的职责呢?
如果一个类具有一个以上的职责,那么就会有多个不同的原因引起该类变化,而这种变化将影响到该类不同职责的使用者(不同用户):
Software entities (classes, modules, function, etc.) should be open for extension, but closed for modification.
软件实体(模块,类,方法等)应该对扩展开放,对修改关闭。
开闭原则是判断面向对象设计是否正确的最基本的原理之一。
根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基类的地方必须能透明地使用其子类的对象。
也就是说,只有满足以下2个条件的OO设计才可被认为是满足了LSP原则:
Clients should not be forced to depend upon interfaces that they do not use.
不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。
它包含了2层意思:
A. High level modules should not depend upon low level modules. Both should depend upon abstractions.
高层模块不应该依赖于低层模块,二者都应该依赖于抽象
B. Abstractions should not depend upon details. Details should depend upon abstractions.
抽象不应该依赖于细节,细节应该依赖于抽象
依赖:在程序设计中,如果一个模块a使用/调用了另一个模块b,我们称模块a依赖模块b。
高层模块与低层模块:往往在一个应用程序中,我们有一些低层次的类,这些类实现了一些基本的或初级的操作,我们称之为低层模块;另外有一些高层次的类,这些类封装了某些复杂的逻辑,并且依赖于低层次的类,这些类我们称之为高层模块。
为什么叫做依赖倒置呢?
面向对象程序设计相对于面向过程(结构化)程序设计而言,依赖关系被倒置了。因为传统的结构化程序设计中,高层模块总是依赖于低层模块。
####问题的提出
Robert C. Martin在依赖倒置原则中给出了“Bad Design”的定义:
为了解决上述问题,Robert C. Martin提出了OO设计的Dependency Inversion Principle原则。
DIP给出了一个解决方案:在高层模块与低层模块之间,引入一个抽象接口层。
High Level Classes(高层模块) --> Abstraction Layer(抽象接口层) --> Low Level Classes(低层模块)
抽象接口是对低层模块的抽象,低层模块继承或实现该抽象接口。
这样,高层模块不直接依赖低层模块,高层模块与低层模块都依赖抽象接口层。
当然,抽象也不依赖低层模块的实现细节,低层模块依赖(继承或实现)抽象定义。
一个对象应当对其他对象有尽可能少的了解
只与你直接的朋友们通信(Only talk to your immediate friends)
不要跟"陌生人"说话(Don’t talk to strangers)
每一个软件单位对其他的单位都只有最少的知识,而且局限于那些本单位密切相关的软件单位.
就是说,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
对于对象 ‘O’ 中一个方法’M’,M应该只能够访问以下对象中的方法:
对象O;
与O直接相关的Component Object;
由方法M创建或者实例化的对象;
作为方法M的参数的对象。
别调用(打电话给)我们,我们会调用(打电话给)你。
回调是这种原则的很好的体现。
也就是说,所有的组件都是被动的,所有的组件初始化和调用都由容器负责。组件处在一个容器当中,由容器负责管理。
简单的来讲,就是由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控。这也就是所谓“控制反转”的概念所在:
不创建对象,而是描述创建对象的方式。
在代码中,对象与服务没有直接联系,而是容器负责将这些联系在一起。
控制权由应用代码中转到了外部容器,控制权的转移,是所谓反转。
好莱坞原则就是IoC(Inversion of Control)或DI(Dependency Injection
)的基础原则。这个原则很像依赖倒置原则,依赖接口,而不是实例,但是这个原则要解决的是怎么把这个实例传入调用类中?你可能把其声明成成员,你可以通过构造函数,你可以通过函数参数。但是IoC可以让你通过配置文件,一个由Service Container
读取的配置文件来产生实际配置的类。但是程序也有可能变得不易读了,程序的性能也有可能还会下降。
Procedural code gets information then makes decisions. Object-oriented code tells objects to do things. — Alec Sharp
过程式程序获取信息然后决策, OO程序则告诉对象做某事情。
也就是说,你应该尽量告诉对象你希望它们去做的事情;而不要询问它们的状态之后做出决定,最后才告诉它们做什么事情。
问题在于,调用方不应该基于被调用对象的状态来做决定,这会导致被调用对象的状态被改变。你正在实现的程序逻辑很可能是被调用对象的职责,而不是调用方本身的,因为你在被调用对象外部做决定破坏了被调用对象的封装。
详见Tell, Don’t Ask
查询:当一个方法返回一个值来回应一个问题的时候,它就具有查询的性质;
命令:当一个方法要改变对象的状态的时候,它就具有命令的性质;
通常,一个方法可能是纯的Command模式或者是纯的Query模式,或者是两者的混合体。在设计接口时,如果可能,应该尽量使接口单一化,保证方法的行为严格的是命令或者是查询,这样查询方法不会改变对象的状态,没有副作用,而会改变对象的状态的方法不可能有返回值。也就是说:如果我们要问一个问题,那么就不应该影响到它的答案。实际应用,要视具体情况而定,语义的清晰性和使用的简单性之间需要权衡。将Command和Query功能合并入一个方法,方便了客户的使用,但是,降低了清晰性,而且,可能不便于基于断言的程序设计并且需要一个变量来保存查询结果。
在系统设计中,很多系统也是以这样原则设计的,查询的功能和命令功能的系统分离,这样有则于系统性能,也有利于系统的安全性。
DbC的核心思想是对软件系统中的元素之间相互合作以及“责任”与“义务”的比喻。这种比喻从商业活动中“客户”与“供应商”达成“契约”而得来。例如:
供应商必须提供某种产品(责任),并且他有权期望客户已经付款(权利)。
客户必须付款(责任),并且有权得到产品(权利)。
契约双方必须履行那些对所有契约都有效的责任,如法律和规定等。
同样的,如果在程序设计中一个模块提供了某种功能,那么它要:
期望所有调用它的客户模块都保证一定的进入条件:这就是模块的先验条件(客户的义务和供应商的权利,这样它就不用去处理不满足先验条件的情况)。
保证退出时给出特定的属性:这就是模块的后验条件——(供应商的义务,显然也是客户的权利)。
在进入时假定,并在退出时保持一些特定的属性:不变式。
契约就是这些权利和义务的正式形式。我们可以用“三个问题”来总结DbC,并且作为设计者要经常问:
它期望的是什么?
它要保证的是什么?
它要保持的是什么?
根据BertrandMeyer氏提出的DBC概念的描述,对于类的一个方法,都有一个前提条件以及一个后续条件,前提条件说明方法接受什么样的参数数据等,只有前提条件得到满足时,这个方法才能被调用;同时后续条件用来说明这个方法完成时的状态,如果一个方法的执行会导致这个方法的后续条件不成立,那么这个方法也不应该正常返回。
现在把前提条件以及后续条件应用到继承子类中,子类方法应该满足:
前提条件不强于基类.
后续条件不弱于基类.
换句话说,通过基类的接口调用一个对象时,用户只知道基类前提条件以及后续条件。因此继承类不得要求用户提供比基类方法要求的更强的前提条件,亦即,继承类方法必须接受任何基类方法能接受的任何条件(参数)。同样,继承类必须顺从基类的所有后续条件,亦即,继承类方法的行为和输出不得违反由基类建立起来的任何约束,不能让用户对继承类方法的输出感到困惑。
这样,我们就有了基于契约的LSP,基于契约的LSP是LSP的一种强化。
在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过这些向对象的委派达到复用已有功能的目的.简单的说就是:要尽量多用聚合,少用继承.
一个包中所有的类应该对同一种类型的变化关闭。一个变化影响一个包,便影响了包中所有的类。一个更简短的说法是:一起修改的类,应该组合在一起(同一个包里)。如果必须修改应用程序里的代码,我们希望所有的修改都发生在一个包里(修改关闭),而不是遍布在很多包里。CCP原则就是把因为某个同样的原因而需要修改的所有类组合进一个包里。如果2个类从物理上或者从概念上联系得非常紧密,它们通常一起发生改变,那么它们应该属于同一个包。
CCP延伸了开闭原则(OCP)的“关闭”概念,当因为某个原因需要修改时,把需要修改的范围限制在一个最小范围内的包里。
包的所有类被一起重用。如果你重用了其中的一个类,就重用全部。换个说法是,没有被一起重用的类不应该被组合在一起。CRP原则帮助我们决定哪些类应该被放到同一个包里。依赖一个包就是依赖这个包所包含的一切。当一个包发生了改变,并发布新的版本,使用这个包的所有用户都必须在新的包环境下验证他们的工作,即使被他们使用的部分没有发生任何改变。因为如果包中包含有未被使用的类,即使用户不关心该类是否改变,但用户还是不得不升级该包并对原来的功能加以重新测试。
CCP则让系统的维护者受益。CCP让包尽可能大(CCP原则加入功能相关的类),CRP则让包尽可能小(CRP原则剔除不使用的类)。它们的出发点不一样,但不相互冲突。
包之间的依赖结构必须是一个直接的无环图形,也就是说,在依赖结构中不允许出现环(循环依赖)。如果包的依赖形成了环状结构,怎么样打破这种循环依赖呢?有2种方法可以打破这种循环依赖关系:第一种方法是创建新的包,如果A、B、C形成环路依赖,那么把这些共同类抽出来放在一个新的包D里。这样就把C依赖A变成了C依赖D以及A依赖D,从而打破了循环依赖关系。第二种方法是使用DIP(依赖倒置原则)和ISP(接口分隔原则)设计原则。
无环依赖原则(ADP)为我们解决包之间的关系耦合问题。在设计模块时,不能有循环依赖。
这个原则简而言之为——只考虑和设计必须的功能,避免过度设计。只实现目前需要的功能,在以后您需要更多功能时,可以再进行添加。
如无必要,勿增复杂性。
软件开发先是一场沟通博弈。
我们的程序员或是架构师在设计系统的时候,会考虑很多扩展性的东西,导致在架构与设计方面使用了大量折衷,最后导致项目失败。这是个令人感到讽刺的教训,因为本来希望尽可能延长项目的生命周期,结果反而缩短了生命周期。
这个原则是UNIX操作系统设计的经典原则,把模块间的耦合降到最低,而努力让一个模块做到精益求精。
内聚:一个模块内各个元素彼此结合的紧密程度
耦合:一个软件结构内不同模块之间互连程度的度量
内聚意味着重用和独立,耦合意味着多米诺效应牵一发动全身。
简单点说,就是将一些公认的配置方式和信息作为内部缺省的规则来使用。例如,Hibernate的映射文件,如果约定字段名和类属性一致的话,基本上就可以不要这个配置文件了。你的应用只需要指定不convention的信息即可,从而减少了大量convention而又不得不花时间和精力啰里啰嗦的东东。配置文件很多时候相当的影响开发效率。
Rails 中很少有配置文件(但不是没有,数据库连接就是一个配置文件),Rails 的fans号称期开发效率是 java 开发的 10
倍,估计就是这个原因。Maven也使用了CoC原则,当你执行mvn
-compile命令的时候,不需要指源文件放在什么地方,而编译以后的class文件放置在什么地方也没有指定,这就是CoC原则。
SoC
是计算机科学中最重要的努力目标之一。这个原则,就是在软件开发中,通过各种手段,将问题的各个关注点分开。如果一个问题能分解为独立且较小的问题,就是相对较易解决的。问题太过于复杂,要解决问题需要关注的点太多,而程序员的能力是有限的,不能同时关注于问题的各个方面。正如程序员的记忆力相对于计算机知识来说那么有限一样,程序员解决问题的能力相对于要解决的问题的复杂性也是一样的非常有限。在我们分析问题的时候,如果我们把所有的东西混在一起讨论,那么就只会有一个结果——乱。
我记得在上一家公司有一个项目,讨论就讨论了1年多,项目本来不复杂,但是没有使用SoC,全部的东西混为一谈,再加上一堆程序员注入了各种不同的观点和想法,整个项目一下子就失控了。最后,本来一个1年的项目做了3年。
实现关注点分离的方法主要有两种,一种是标准化,另一种是抽象与包装。标准化就是制定一套标准,让使用者都遵守它,将人们的行为统一起来,这样使用标准的人就不用担心别人会有很多种不同的实现,使自己的程序不能和别人的配合。Java
EE就是一个标准的大集合。每个开发者只需要关注于标准本身和他所在做的事情就行了。就像是开发镙丝钉的人只专注于开发镙丝钉就行了,而不用关注镙帽是怎么生产的,反正镙帽和镙丝钉按标来就一定能合得上。不断地把程序的某些部分抽像差包装起来,也是实现关注点分离的好方法。一旦一个函数被抽像出来并实现了,那么使用函数的人就不用关心这个函数是如何实现的,同样的,一旦一个类被抽像并实现了,类的使用者也不用再关注于这个类的内部是如何实现的。诸如组件,分层,面向服务,等等这些概念都是在不同的层次上做抽像和包装,以使得使用者不用关心它的内部实现细节。
说白了还是“高内聚,低耦合”。