目录:java 定义:不要存在多于一个致使类变动的缘由。通俗的说,即一个类只负责一项职责。 解决方案:遵循单一职责原则。分别创建两个类T一、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。字体 说到单一职责原则,不少人都会不屑一顾。由于它太简单了。稍有经验的程序员即便历来没有读过设计模式、历来没有据说过单一职责原则,在设计软件时也会自觉的遵照这一重要原则,由于这是常识。在软件编程中,谁也不但愿由于修改了一个功能致使其余的功能发生故障。而避免出现这一问题的方法即是遵循单一职责原则。虽然单一职责原则如此简单,而且被认为是常识,可是即使是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为何会出现这种现象呢?由于有职责扩散。所谓职责扩散,就是由于某种缘由,职责P被分化为粒度更细的职责P1和P2。 好比:类T只负责一个职责P,这样设计是符合单一职责原则的。后来因为某种缘由,也许是需求变动了,也许是程序的设计者境界提升了,须要将职责P细分为粒度更细的职责P1,P2,这时若是要使程序遵循单一职责原则,须要将类T也分解为两个类T1和T2,分别负责P一、P2两个职责。可是在程序已经写好的状况下,这样作简直太费时间了。因此,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样作有悖于单一职责原则。(这样作的风险在于职责扩散的不肯定性,由于咱们不会想到这个职责P,在将来可能会扩散为P1,P2,P3,P4……Pn。因此记住,在职责扩散到咱们没法控制的程度以前,马上对代码进行重构。) 举例说明,用一个类描述动物呼吸这个场景:
运行结果: 牛呼吸空气 羊呼吸空气 猪呼吸空气 程序上线后,发现问题了,并非全部的动物都呼吸空气的,好比鱼就是呼吸水的。修改时若是遵循单一职责原则,须要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码以下:
运行结果: 牛呼吸空气 羊呼吸空气 猪呼吸空气 鱼呼吸水 咱们会发现若是这样修改花销是很大的,除了将原来的类分解以外,还须要修改客户端。而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多,代码以下:
能够看到,这种修改方式要简单的多。可是却存在着隐患:有一天须要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又须要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患倒是最大的。还有一种修改方式:
能够看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上倒是符合单一职责原则的,由于它并无动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?其实这真的比较难说,须要根据实际状况来肯定。个人原则是:只有逻辑足够简单,才能够在代码级别上违反单一职责原则;只有类中方法数量足够少,才能够在方法级别上违反单一职责原则; 例如本文所举的这个例子,它太简单了,它只有一个方法,因此,不管是在代码级别上违反单一职责原则,仍是在方法级别上违反,都不会形成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而须要修改类时,除非这个类自己很是简单,不然仍是遵循单一职责原则的好。 遵循单一职责原的优势有:
须要说明的一点是单一职责原则不仅是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。 确定有很多人跟我刚看到这项原则的时候同样,对这个原则的名字充满疑惑。其实缘由就是这项原则最先是在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须要完成两个功能:
因为类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点:
依赖倒置原则的核心就是要咱们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。 定义:客户端不该该依赖它不须要的接口;一个类对另外一个类的依赖应该创建在最小的接口上。 解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们须要的接口创建依赖关系。也就是采用接口隔离原则。 举例来讲明接口隔离原则: (图1 未遵循接口隔离原则的设计) 这个图的意思是:类A依赖接口I中的方法一、方法二、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法一、方法四、方法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项原则,偏偏是告诉咱们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉咱们实现类要职责单一;里氏替换原则告诉咱们不要破坏继承体系;依赖倒置原则告诉咱们要面向接口编程;接口隔离原则告诉咱们在设计接口的时候要精简单一;迪米特法则告诉咱们要下降耦合。而开闭原则是总纲,他告诉咱们要对扩展开放,对修改关闭。 最后说明一下如何去遵照这六个原则。对这六个原则的遵照并非是和否的问题,而是多和少的问题,也就是说,咱们通常不会说有没有遵照,而是说遵照程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是同样,制定这六个原则的目的并非要咱们刻板的遵照他们,而须要根据实际状况灵活运用。对他们的遵照程度只要在一个合理的范围内,就算是良好的设计。咱们用一幅图来讲明一下。 图中的每一条维度各表明一项原则,咱们依据对这项原则的遵照程度在维度上画一个点,则若是对这项原则遵照的合理的话,这个点应该落在红色的同心圆内部;若是遵照的差,点将会在小圆内部;若是过分遵照,点将会落在大圆外部。一个良好的设计体如今图中,应该是六个顶点都在同心圆中的六边形。 在上图中,设计一、设计2属于良好的设计,他们对六项原则的遵照程度都在合理的范围内;设计三、设计4设计虽然有些不足,但也基本能够接受;设计5则严重不足,对各项原则都没有很好的遵照;而设计6则遵照过渡了,设计5和设计6都是迫切须要重构的设计。 到这里,设计模式的六大原则就写完了。主要参考书籍有《设计模式》《设计模式之禅》《大话设计模式》以及网上一些零散的文章,但主要内容主要仍是我本人对这六个原则的感悟。写出来的目的一方面是对这六项原则系统地整理一下,一方面也与广大的网友分享,由于设计模式对编程人员来讲,的确很是重要。正若有句话叫作一千个读者眼中有一千个哈姆雷特,若是你们对这六项原则的理解跟我有所不一样,欢迎留言,你们共同探讨。 |