定义:不要存在多于一个导致类变更的原因。
通俗的说。即一个类仅仅负责一项职责。 问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而须要改动类T时,有可能会导致原本执行正常的职责P2功能发生问题。
解决方式:遵循单一职责原则。分别建立两个类T1、T2,使T1完毕职责P1功能。T2完毕职责P2功能。
这样,当改动类T1时,不会使职责P2发生问题风险;同理,当改动T2时,也不会使职责P1发生问题风险。
说到单一职责原则。非常多人都会不屑一顾。
由于它太简单了。稍有经验的程序猿即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,由于这是常识。在软件编程中,谁也不希望由于改动了一个功能导致其它的功能发生问题。
而避免出现这一问题的方法便是遵循单一职责原则。尽管单一职责原则如此简单。而且被觉得是常识。可是即便是经验丰富的程序猿写出的程序。也会有违背这一原则的代码存在。为什么会出现这样的现象呢?由于有职责扩散。
所谓职责扩散,就是由于某种原因,职责P被分化为粒度更细的职责P1和P2。
比方:类T仅仅负责一个职责P,这样设计是符合单一职责原则的。
后来由于某种原因,或许是需求变更了,或许是程序的设计者境地提高了,须要将职责P细分为粒度更细的职责P1,P2,这时假设要使程序遵循单一职责原则,须要将类T也分解为两个类T1和T2。分别负责P1、P2两个职责。可是在程序已经写好的情况下。这样做简直太费时间了。
所以,简单的改动类T,用它来负责两个职责是一个比較不错的选择。尽管这样做有悖于单一职责原则。
(这样做的风险在于职责扩散的不确定性。由于我们不会想到这个职责P,在未来可能会扩散为P1,P2。P3。P4……Pn。所以记住。在职责扩散到我们无法控制的程度之前,立马对代码进行重构。)
举例说明,用一个类描写叙述动物呼吸这个场景:
执行结果:
牛呼吸空气
羊呼吸空气
猪呼吸空气
程序上线后,发现问题了。并非全部的动物都呼吸空气的,比方鱼就是呼吸水的。
改动时假设遵循单一职责原则,须要将Animal类细分为陆生动物类Terrestrial。水生动物Aquatic,代码例如以下:
执行结果:
牛呼吸空气
羊呼吸空气
猪呼吸空气
鱼呼吸水
我们会发现假设这样改动花销是非常大的,除了将原来的类分解之外,还须要改动client。而直接改动类Animal来达成目的尽管违背了单一职责原则,但花销却小的多。代码例如以下:
能够看到,这样的改动方式要简单的多。可是却存在着隐患:有一天须要将鱼分为呼吸淡水的鱼和呼吸海水的鱼。则又须要改动Animal类的breathe方法。而对原有代码的改动会对调用“猪”“牛”“羊”等相关功能带来风险,或许某一天你会发现程序执行的结果变为“牛呼吸水”了。这样的改动方式直接在代码级别上违背了单一职责原则,尽管改动起来最简单,但隐患却是最大的。
另一种改动方式:
能够看到。这样的改动方式没有改动原来的方法。而是在类中新加了一个方法,这样尽管也违背了单一职责原则。但在方法级别上却是符合单一职责原则的。由于它并没有动原来方法的代码。
这三种方式各有优缺点。那么在实际编程中,採用哪一种呢?事实上这真的比較难说,须要依据实际情况来确定。我的原则是:仅仅有逻辑足够简单,才干够在代码级别上违反单一职责原则。仅仅有类中方法数量足够少,才干够在方法级别上违反单一职责原则;
比如本文所举的这个样例,它太简单了,它仅仅有一个方法,所以。不管是在代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。
实际应用中的类都要复杂的多,一旦发生职责扩散而须要改动类时,除非这个类本身非常easy,否则还是遵循单一职责原则的好。
遵循单一职责原的长处有:
能够减少类的复杂度,一个类仅仅负责一项职责。其逻辑肯定要比负责多项职责简单的多; -提高类的可读性。提高系统的可维护性;
变更引起的风险减少,变更是必定的。假设单一职责原则遵守的好,当改动一个功能时,能够显著减少对其它功能的影响。 须要说明的一点是单一职责原则不仅仅是面向对象编程思想所特有的,仅仅要是模块化的程序设计,都适用单一职责原则。
肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑。事实上原因就是这项原则最早是在1988年。由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。
定义1:假设对每一个类型为 T1的对象 o1。都有类型为 T2 的对象o2。使得以 T1定义的全部程序 P 在全部的对象 o1 都代换成 o2 时。程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
定义2:全部引用基类的地方必须能透明地使用其子类的对象。
问题由来:有一功能P1,由类A完毕。
现须要将功能P1进行扩展,扩展后的功能为P,当中P由原有功能P1与新功能P2组成。
新功能P由类A的子类B来完毕。则子类B在完毕新功能P2的同一时候,有可能会导致原有功能P1发生问题。
解决方式:当使用继承时。遵循里氏替换原则。类B继承类A时,除加入新的方法完毕新增功能P2外。尽量不要重写父类A的方法。也尽量不要重载父类A的方法。
继承包括这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,尽管它不强制要求全部的子类必须遵从这些契约,可是假设子类对这些非抽象方法随意改动。就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
继承作为面向对象三大特性之中的一个。在给程序设计带来巨大便利的同一时候。也带来了弊端。比方使用继承会给程序带来侵入性,程序的可移植性减少,添加了对象间的耦合性,假设一个类被其它的类所继承,则当这个类须要改动时。必须考虑到全部的子类,而且父类改动后。全部涉及到子类的功能都有可能会产生故障。
举例说明继承的风险。我们须要完毕一个两数相减的功能,由类A来负责。
执行结果:
100-50=50
100-80=20
后来。我们须要添加一个新的功能:完毕两数相加,然后再与100求和,由类B来负责。即类B须要完毕两个功能:
- 两数相减。
- 两数相加,然后再加100。
由于类A已经实现了第一个功能,所以类B继承类A后。仅仅须要再完毕第二个功能就能够了,代码例如以下:
类B完毕后。执行结果:
100-50=150
100-80=180
100+20+100=220
我们发现原本执行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成全部执行相减功能的代码全部调用了类B重写后的方法,造成原本执行正常的功能出现了错误。在本例中。引用基类A完毕的功能,换成子类B之后,发生了异常。
在实际编程中。我们经常会通过重写父类的方法来完毕新的功能,这样写起来尽管简单,可是整个继承体系的可复用性会比較差,特别是运用多态比較频繁时,程序执行出错的几率非常大。假设非要重写父类的方法。比較通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,採用依赖、聚合,组合等关系取代。
氏替换原则通俗的来讲就是:子类能够扩展父类的功能,但不能改变父类原有的功能。
它包括下面4层含义:
子类能够实现父类的抽象方法,但不能覆盖父类的非抽象方法。
子类中能够添加自己特有的方法。
当子类的方法重载父类的方法时,方法的前置条件(即方法的形參)要比父类方法的输入參数更宽松。
当子类的方法实现父类的抽象方法时。方法的后置条件(即方法的返回值)要比父类更严格。
看上去非常不可思议,由于我们会发如今自己编程中经常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?
后果就是:你写的代码出问题的几率将会大大添加。
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象。抽象不应该依赖细节;细节应该依赖抽象。
问题由来:类A直接依赖类B,假如要将类A改为依赖类C。则必须通过改动类A的代码来达成。
这样的场景下,类A通常是高层模块。负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作。假如改动类A,会给程序带来不必要的风险。
解决方式:将类A改动为依赖接口I。类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大减少改动类A的几率。
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。
以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是详细的实现类。使用接口或者抽象类的目的是制定好规范和契约,而不去涉及不论什么详细的操作,把展现细节的任务交给他们的实现类去完毕。
依赖倒置原则的核心思想是面向接口编程。我们依然用一个样例来说明面向接口编程比相对于面向实现编程好在什么地方。场景是这样的。母亲给孩子讲故事,仅仅要给她一本书,她就能够照着书给孩子讲故事了。
代码例如以下:
执行结果:
妈妈開始讲故事
非常久非常久曾经有一个阿拉伯的故事……
执行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码例如以下:
这位母亲却办不到,由于她竟然不会读报纸上的故事。这太荒唐了,仅仅是将书换成报纸,竟然必须要改动Mother才干读。
假如以后需求换成杂志呢?换成网页呢?还要不断地改动Mother,这显然不是好的设计。
原因就是Mother与Book之间的耦合性太高了。必须减少他们之间的耦合度才行。
我们引入一个抽象的接口IReader。读物,仅仅要是带字的都属于读物:
Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴。他们各自都去实现IReader接口,这样就符合依赖倒置原则了。代码改动为:
执行结果:
妈妈開始讲故事
非常久非常久曾经有一个阿拉伯的故事……
妈妈開始讲故事
林书豪17+9助尼克斯击败老鹰……
这样改动后。不管以后怎样扩展Client类,都不须要再改动Mother类了。这仅仅是一个简单的样例,实际情况中,代表高层模块的Mother类将负责完毕基本的业务逻辑,一旦须要对它进行改动。引入错误的风险极大。所以遵循依赖倒置原则能够减少类之间的耦合性,提高系统的稳定性。减少改动程序造成的风险。
採用依赖倒置原则给多人并行开发带来了极大的便利。比方上例中。原本Mother类与Book类直接耦合时,Mother类必须等Book类编码完毕后才干够进行编码。由于Mother类依赖于Book类。改动后的程序则能够同一时候开工,互不影响。由于Mother与Book类一点关系也没有。參与协作开发的人越多、项目越庞大,採用依赖导致原则的意义就越重大。如今非常流行的TDD开发模式就是依赖倒置原则最成功的应用。
传递依赖关系有三种方式,以上的样例中使用的方法是接口传递,另外还有两种传递方式:构造方法传递和setter方法传递。相信用过Spring框架的。对依赖的传递方式一定不会陌生。
在实际编程中。我们一般须要做到例如以下3点:
- 低层模块尽量都要有抽象类或接口。或者两者都有。
- 变量的声明类型尽量是抽象类或接口。
- 使用继承时遵循里氏替换原则。
依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。
定义:client不应该依赖它不须要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
问题由来:类A通过接口I依赖类B。类C通过接口I依赖类D,假设接口I对于类A和类B来说不是最小接口。则类B和类D必须去实现他们不须要的方法。
解决方式:将臃肿的接口I拆分为独立的几个接口。类A和类C分别与他们须要的接口建立依赖关系。也就是採用接口隔离原则。
举例来说明接口隔离原则:
(图1 未遵循接口隔离原则的设计)
这个图的意思是:类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。
类C依赖接口I中的方法1、方法4、方法5。类D是对类C依赖的实现。对于类B和类D来说。尽管他们都存在着用不到的方法(也就是图中红色字体标记的方法)。但由于实现了接口I,所以也必须要实现这些用不到的方法。对类图不熟悉的能够參照程序代码来理解,代码例如以下:
能够看到,假设接口过于臃肿,仅仅要接口中出现的方法。不管对依赖于它的类有没实用处,实现类中都必须去实现这些方法,这显然不是好的设计。假设将这个设计改动为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口,拆分后的设计如图2所看到的:
(图2 遵循接口隔离原则的设计)
照例贴出程序的代码,供不熟悉类图的朋友參考:
接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口。尽量细化接口。接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口。而不要试图去建立一个非常庞大的接口供全部依赖它的类去调用。本文样例中,将一个庞大的接口变更为3个专用的接口所採用的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,能够预防外来变更的扩散,提高系统的灵活性和可维护性。
讲到这里。非常多人会觉的接口隔离原则跟之前的单一职责原则非常类似,事实上不然。其一,单一职责原则原注重的是职责。而接口隔离原则注重对接口依赖的隔离。
其二。单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节。而接口隔离原则主要约束接口接口,主要针对抽象,针对程序总体框架的构建。
採用接口隔离原则对接口进行约束时。要注意下面几点:
接口尽量小,可是要有限度。对接口进行细化能够提高程序设计灵活性是不挣的事实,可是假设过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
为依赖接口的类定制服务。仅仅暴露给调用的类它须要的方法。它不须要的方法则隐藏起来。仅仅有专注地为一个模块提供定制服务,才干建立最小的依赖关系。
提高内聚,减少对外交互。使接口用最少的方法去完毕最多的事情。
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。
设计接口的时候,仅仅有多花些时间去思考和筹划。才干准确地实践这一原则。
定义:一个对象应该对其它对象保持最少的了解。
问题由来:类与类之间的关系越密切。耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
解决方式:尽量减少类与类之间的耦合。
自从我们接触编程開始。就知道了软件编程的总的原则:低耦合,高内聚。不管是面向过程编程还是面向对象编程。仅仅有使各个模块之间的耦合尽量的低。才干提高代码的复用率。
低耦合的长处不言而喻。可是怎么样编程才干做到低耦合呢?那正是迪米特法则要去完毕的。
迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说。对于被依赖的类来说,不管逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不正确外泄漏不论什么信息。迪米特法则另一个更简单的定义:仅仅与直接的朋友通信。
首先来解释一下什么是直接的朋友:每一个对象都会与其它对象有耦合关系,仅仅要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式非常多,依赖、关联、组合、聚合等。
当中,我们称出现成员变量、方法參数、方法返回值中的类为直接的朋友。而出如今局部变量中的类则不是直接的朋友。也就是说。陌生的类最好不要作为局部变量的形式出如今类的内部。
举一个样例:有一个集团公司。下属单位有分公司和直属部门,如今要求打印出全部下属单位的员工ID。先来看一下违反迪米特法则的设计。
如今这个设计的主要问题出在CompanyManager中。依据迪米特法则,仅仅与直接的朋友发生通信,而SubEmployee类并非CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司仅仅与他的分公司耦合即可了,与分公司的员工并没有不论什么联系,这样设计显然是添加了不必要的耦合。依照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。
改动后的代码例如以下:
改动后。为分公司添加了打印人员ID的方法,总公司直接调用来打印。从而避免了与分公司的员工发生耦合。
迪米特法则的初衷是减少类之间的耦合,由于每一个类都减少了不必要的依赖,因此的确能够减少耦合关系。
可是凡事都有度,尽管能够避免与非直接的类通信。可是要通信,必定会通过一个“中介”来发生联系,比如本例中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。过分的使用迪米特原则。会产生大量这样的中介和传递类,导致系统复杂度变大。
所以在採用迪米特法则时要重复权衡,既做到结构清晰。又要高内聚低耦合。
定义:一个软件实体如类、模块和函数应该对扩展开放,对改动关闭。
问题由来:在软件的生命周期内。由于变化、升级和维护等原因须要对软件原有代码进行改动时,可能会给旧代码中引入错误,也可能会使我们不得不正确整个功能进行重构,而且须要原有代码经过又一次測试。
解决方式:当软件须要变化时。尽量通过扩展软件实体的行为来实现变化,而不是通过改动已有的代码来实现变化。
开闭原则是面向对象设计中最基础的设计原则,它指导我们怎样建立稳定灵活的系统。开闭原则可能是设计模式六项原则中定义最模糊的一个了,它仅仅告诉我们对扩展开放。对改动关闭,可是究竟怎样才干做到对扩展开放。对改动关闭,并没有明白的告诉我们。曾经。假设有人告诉我“你进行设计的时候一定要遵守开闭原则”,我会觉的他什么都没说。但貌似又什么都说了。由于开闭原则真的太虚了。
在细致思考以及细致阅读非常多设计模式的文章后,最终对开闭原则有了一点认识。事实上,我们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说。仅仅要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的。这个开闭原则更像是前面五项原则遵守程度的“平均得分”,前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好。假设前面5项原则遵守的不好。则说明开闭原则遵守的不好。
事实上笔者觉得,开闭原则无非就是想表达这样一层意思:用抽象构建框架。用实现扩展细节。由于抽象灵活性好,适应性广。仅仅要抽象的合理,能够基本保持软件架构的稳定。而软件中易变的细节。我们用从抽象派生的实现类来进行扩展,当软件须要发生变化时。我们仅仅须要依据需求又一次派生一个实现类来扩展就能够了。
当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。
讲到这里,再回忆一下前面说的5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一。迪米特法则告诉我们要减少耦合。而开闭原则是总纲。他告诉我们要对扩展开放,对改动关闭。
最后说明一下怎样去遵守这六个原则。对这六个原则的遵守并非是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。不论什么事都是过犹不及。设计模式的六个设计原则也是一样。制定这六个原则的目的并非要我们刻板的遵守他们。而须要依据实际情况灵活运用。对他们的遵守程度仅仅要在一个合理的范围内,就算是良好的设计。