转发地址:http://www.cnblogs.com/firstdream/p/7101289.htmlhtml
不要存在多于一个致使类变动的缘由。通俗的说,即一个类只负责一项职责。java
类T负责两个不一样的职责:职责P1,职责P2。当因为职责P1需求发生改变而须要修改类T时,有可能会致使本来运行正常的职责P2功能发生故障。git
遵循单一职责原则。分别创建两个类T一、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。程序员
说到单一职责原则,不少人都会不屑一顾。由于它太简单了。稍有经验的程序员即便历来没有读过设计模式、历来没有据说过单一职责原则,在设计软件时也会自觉的遵照这一重要原则,由于这是常识。在软件编程中,谁也不但愿由于修改了一个功能致使其余的功能发生故障。而避免出现这一问题的方法即是遵循单一职责原则。虽然单一职责原则如此简单,而且被认为是常识,可是即使是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为何会出现这种现象呢?由于有职责扩散。所谓职责扩散,就是由于某种缘由,职责P被分化为粒度更细的职责P1和P2。web
好比:类T只负责一个职责P,这样设计是符合单一职责原则的。后来因为某种缘由,也许是需求变动了,也许是程序的设计者境界提升了,须要将职责P细分为粒度更细的职责P1,P2,这时若是要使程序遵循单一职责原则,须要将类T也分解为两个类T1和T2,分别负责P一、P2两个职责。可是在程序已经写好的状况下,这样作简直太费时间了。因此,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样作有悖于单一职责原则。(这样作的风险在于职责扩散的不肯定性,由于咱们不会想到这个职责P,在将来可能会扩散为P1,P2,P3,P4……Pn。因此记住,在职责扩散到咱们没法控制的程度以前,马上对代码进行重构。)编程
举例说明,用一个类描述动物呼吸这个场景:后端
class Animal{ public void breathe(String animal){ System.out.println(animal+"呼吸空气"); } } public class Client{ public static void main(String[] args){ Animal animal = new Animal(); animal.breathe("牛"); animal.breathe("羊"); animal.breathe("猪"); } }
运行结果:设计模式
牛呼吸空气 羊呼吸空气 猪呼吸空气
程序上线后,发现问题了,并非全部的动物都呼吸空气的,好比鱼就是呼吸水的。修改时若是遵循单一职责原则,须要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码以下:网络
class Terrestrial{ public void breathe(String animal){ System.out.println(animal+"呼吸空气"); } } class Aquatic{ public void breathe(String animal){ System.out.println(animal+"呼吸水"); } } public class Client{ public static void main(String[] args){ Terrestrial terrestrial = new Terrestrial(); terrestrial.breathe("牛"); terrestrial.breathe("羊"); terrestrial.breathe("猪"); Aquatic aquatic = new Aquatic(); aquatic.breathe("鱼"); } }
运行结果:架构
牛呼吸空气 羊呼吸空气 猪呼吸空气 鱼呼吸水
咱们会发现若是这样修改花销是很大的,除了将原来的类分解以外,还须要修改客户端。而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多,代码以下:
class Animal{ public void breathe(String animal){ if("鱼".equals(animal)){ System.out.println(animal+"呼吸水"); }else{ System.out.println(animal+"呼吸空气"); } } } public class Client{ public static void main(String[] args){ Animal animal = new Animal(); animal.breathe("牛"); animal.breathe("羊"); animal.breathe("猪"); animal.breathe("鱼"); } }
能够看到,这种修改方式要简单的多。可是却存在着隐患:有一天须要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又须要修改Animal类的breathe方法,而对原有代码的修改会对调用"猪""牛""羊"等相关功能带来风险,也许某一天你会发现程序运行的结果变为"牛呼吸水"了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患倒是最大的。还有一种修改方式:
class Animal{ public void breathe(String animal){ System.out.println(animal+"呼吸空气"); } public void breathe2(String animal){ System.out.println(animal+"呼吸水"); } } public class Client{ public static void main(String[] args){ Animal animal = new Animal(); animal.breathe("牛"); animal.breathe("羊"); animal.breathe("猪"); animal.breathe2("鱼"); } }
能够看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上倒是符合单一职责原则的,由于它并无动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?其实这真的比较难说,须要根据实际状况来肯定。个人原则是:只有逻辑足够简单,才能够在代码级别上违反单一职责原则;只有类中方法数量足够少,才能够在方法级别上违反单一职责原则;
例如本文所举的这个例子,它太简单了,它只有一个方法,因此,不管是在代码级别上违反单一职责原则,仍是在方法级别上违反,都不会形成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而须要修改类时,除非这个类自己很是简单,不然仍是遵循单一职责原则的好。
遵循单一职责原的优势有:
须要说明的一点是单一职责原则不仅是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
陈述:
就一个类而言,应该只有一个致使其变化的缘由
分析: 一个职责就是一个变化的轴线。 一个类若是承担的职责过多,就等于将这些职责耦合在一块儿。一个职责的变化可能会虚弱或者抑止这个类完成其它职责的能力。
多职责将致使脆弱性的臭味。
示例1:
Rectangle类具备两个职责: 计算矩形面积的数学模型 将矩形在一个图形设备上描述出来
Rectangle类违反了SRP,具备两个职能——计算面积和绘制矩形 这种对SRP的违反将致使两个方面的问题: 包含没必要要的代码 一个应用可能但愿使用Retangle类计算矩形的面积,可是却被迫将绘制矩形相关的代码也包含进来 一些逻辑上毫无关联的缘由可能致使应用失败 若是GraphicalApplication的需求发生了变化,从而对Rectangle类进行了修改。可是这样的变化竟然会要求咱们从新构建、测试以及部署ComputationalGeometryApplication,不然其将莫名其妙的失败。
修改后的设计以下:
示例2: 一个Modem的接口:
Class Modem{ public: virtual void dail(char* pno)=0; virtual void hangup( ) =0; virtual void send(char c) =0; virtual void recv( ) =0; };
Modem类(可能)有两个职责:
最近在实践微服务化过程当中,对其“单一职责”原则深有体会。那么只有微服务化才能够单一职责,才能够解耦吗?答案是否认的。
单一职责原则是这样定义的:单一的功能,而且彻底封装起来。
咱们作后端Java开发的,应该最熟悉的就是标准的3层架构了,尤为是使用Spring.io体系的:Controller、Service、Dao/Repository。为何要分层?就是为了保证单一职责,数据模型的事情交给Controller,业务逻辑的事情交给Service,和数据打交道的事情就交给Dao/Repository。有时候或者有些人会分层分的更多,4层,5层,我本身也这样干过,说白了也是为了保证单一职责,3层不能知足单一职责了,耦合度高了,就分。
咱们都知道一个webapp在通过必定时间的开发后,就惨不忍睹,即使是有标准的分层,页面或模板文件一大堆,最初的很清晰的3层标准架构也变味了,Controller,Service,Dao/Repository各层之间、Service之间、Dao/Repository之间互相调用,一团乱麻。这个时候没改一行代码都有可能一个老鼠害了一锅汤,bug就如同蚂蚁洞。
这些问题最后就形成:
为了解决这些问题,就须要时时刻刻清楚的记住“单一职责”,单一职责能够用到软件开发的任何地方。
应该说职责分离来解耦是最经常使用最有效的架构方法,这可以很大限度的简化一切。
下面就从软件开发、设计、架构,以及重构/演进/进化,从小到大几个方面来讲说单一职责:
这应该是最小的能体现单一职责的程序单元了。最熟悉的最典型的莫过于Helper/Utils类方法了,但这种类方法的特征很明显,也很容易遵循单一职责,99%以上的开发人员均可以作到。但不只仅这样的类方法要遵循单一职责原则,每个类方法都应该遵循单一职责原则,尤为是一些处理业务逻辑的类方法更要遵循单一职责原则,处理业务的类方法一般要配合类的单一职责原则进行,下节中讨论。
所以,这也是为何不少Team Leader要求类方法代码行数保持在20行左右,其实就是为了保证单一职责,20行左右是一个经验粗略数字,固然,10行或者30行来完成类方法也是能够的。大部分单一职责的类方法用20行左右的代码就够了,若是超过20行就要考虑是否保证了单一职责了。那咱们在迭代重构的过程当中就要考虑拆分这样的类方法来保证单一职责。
类方法的单一职责是最单纯的,很具体的,不掺杂任何额外信息,只关心输入、输出、和职责;必定要明确地定义类方法的职责,保证在迭代中不被错误的扩展,不被调用者错误地使用。
要用面向对象的设计方法,单一职责原则来定义类。开发人员必定要很好地理解“单一职责原则”,具备面向对象的抽象思惟能力。
当在迭代中一个类过于庞大或者快速膨胀,说明已经有坏味道了,这时候就须要考虑用单一职责原则或者面向对象的分析方法来重构和从新定义类了,一般就是要抽象和拆分类,不然未来会变成一个方法容器。
把类比做一我的,她的职责就是完成本身职责范围内的事情,若是她什么事情都管,就叫多管闲事,能够想象她多管闲事的后果,会搅得鸡犬不宁。一样,类也是,类若是多管闲事,那会搅得整个应用不稳定,漏洞百出,还很难修复。因此说定义一个类,要明确这个类的职责。使用面向对象的分析和设计方法,能很好地准肯定义一个类的职责范围,一般会用到封装、继承、多态和抽象等设计方法。
分层就是最经常使用的架构方法之一,分层具体体如今分包和分类,就是分门别类的意思。俗话说,物以类聚,人以群分。
包结构在单一职责原则上是类的补充,职责范围进一步扩大。若是把一个类叫作一我的,那么包就是一个最小单位的团队,职责就是负责一类特定事情。 如何分包呢?那就要用到分类学的知识了,要以什么特征来分,可能不只仅只有一种特征,好比,先用公司域名来作基础包名,这里叫一级包名;而后再用一个特定的有意义的标识做为二级子包名;以后按分层(web,dao,service等等)方法作三级包名,也能够先按照业务再按分层。例如:
域名:tietang.wang 有个项目叫:social 那么我能够这样分: wang.tietang - social - web - service - dao - commons 也能够这样: wang.tietang - commons - user - web - service - dao - relation - web - service - dao
一般以多maven module或者gradle 多module形式存在,来保证单一职责。
当业务量尚未达到服务拆分的火候,一般在一个APP发展的太庞大时或者在工程建设初期时,须要规范和整理项目结构。这个时候须要多工程从文件系统上隔离,经过module依赖来集成。须要注意的是这样的架构或拆分不是随意的,要以单一职责原则来拆分,更具体一点就是要根据业务、技术框架功能等特性来拆分。
好比,按技术组件拆分,一般会有一些技术组件,能够把她放到commons module,若是有多种类型的技术组件,就拆分为commons module的子module;也能够直接将这些技术组件拆分为独立的工程,存在于独立的git/svn仓库,独立管理,专人负责,其余哪些module须要就依赖她。那拆分的这些技术组件的每个应该遵循单一职责原则,例如数据分片的框架、NIO基础网络框架等等。
好比,按业务拆分,例若有用户、订单、商品、支付,那么就按照这些业务拆分为子module,每个子module就只负责本身的业务逻辑,也遵循单一职责。
那每一个module的职责范围又比类和包更大,这个时候职责也更模糊,有时候很难把握,对于技术组件可能相对清晰,而业务module就要熟悉业务,明确业务边界。
多module拆分后也是为未来服务化埋下伏笔,同时在物理文件系统比较清晰了,那在依赖管理上也要掌握好保持清晰的依赖逻辑,把握好单一职责原则。
微服务,从运行时隔离,但业务量发展到必定时候,从单体或者多module工程拆分或演化出来,可独立打包可独立部署并复合单一原则的application,固然了微服务所体现的价值不只仅是隔离和独立部署,还有不少这里能够参考单体应用与微服务优缺点辨析。单一职责在微服务中的价值是最重要的,包含了app层面和开发app的团队层面,微服务的大部分优势均可以围绕单一职责来展开。
先引用《韩非子·扬权》中的一段文字:
夫物者有所宜,材者有所施,各处其宜,故上下无为。 使鸡司夜,令狸执鼠,皆用其能,上乃无事。 上有所长,事乃不方。 矜而好能,下之所欺:辩惠好生,下因其材。 上下易用,国故不治。
各得其所,各司其职。因此,团队也要遵循单一职责原则,这样才能很好地管理团队成员的时间,提升效率。一我的专一作一件事情的效率远高于同时关注多件事情。一样一我的一直管理和维护同一份代码要比多人同时维护多份代码的效率高不少。每个人都有本身的个性,他有本身的擅长,让每个人专一本身擅长的事情,那确定事半功倍,整个团队绩效确定也很突出。
总之,引用古文名句说明了全部:
为何单一职责原则(SRP)是最难运用的 单一职责原则(SRP)已经几乎是每个程序员都知道的设计原则。最先由Robert C. Martin在<<敏捷软件开发 — 原则、模式与实践>>中正式提出。书中做者在结论中提到: SRP是全部设计原则最简单的,但也是最难运用的。(中文翻译有之一,略去了)
现实工做中,关于一个类是否符合SRP,或者是否有必要符合SRP的讨论是常常发生的。争论的关键在于职责的定义,但我理解SRP真正的核心是关注于变化。这并非个人新看法,全是来自Martin大叔的解释:
他的提醒是很是中肯的。实践中正是经常基于功能的分类来定义职责的。
举个例子。假如咱们要开发一个学校的教职员工管理系统。须要定义一个教师员工的类(炒菜的师傅先就不考虑了),考虑到老师和班主任两个角色,一般会认为他有两类职责: . 教师 (班主任极可能会带课) . 班级的管理 (组织班委,整治一下早恋之类的)
这时你拿着设计到了一个寄宿学校,校长可能会告诉你,他们这里的教师会轮流值班,兼作保育员,照看住校的学生。又是一个新的职责,怎么办?
若是遵照单一职责的原则,咱们应该增长一个接口:
果然要如此吗? 注意,若是是在通常的学校,保育员不是老师的本职工做,可在这所寄宿学校里,倒是教师的本职工做,是和老师一块儿变化的。校长的反馈是:
“咱们学校的教师必须担任保育工做,我并不认为这会是什么新职责。做为教师,要么接受,要么离开。至于班主任工做,确实仍是其特殊的地方,否则也不会给担任班主任的老师多一点津贴了。”。
请再体会一下,关于保育员职责的讨论。若是两个职责/角色不是同时变化的,才考虑分离。若是肯定同时变化,就没有必要分离。除非有一天,某个劳动部门到该寄宿学校检查,认为他们这样不符合某个法律规定,强制规定老师能够选择是否担当保育员。如此一来,两个职责就又变成独立变化的了,就能够考虑分离职责。
再进一步,若是是针对一个只有一个支教教师的小学,极为偏僻。这里的校长会告诉你:
”这个学校里的每个教师,惟一的一个,既是校长,也是老师。我不认为还须要明确班主任作什么,教师作什么,在这里,只要学生须要的都要作。而且这里很穷,五年内都不见得再有新老师来。”。
这个感人的故事告诉咱们,在这所学校里, 他不在了,这所学校也就不在了,彻底没有什么相对的变化,也没有什么能够确认的变化。因此在这里的管理系统里,教职员工只有一个实例类.
确定有很多人跟我刚看到这项原则的时候同样,对这个原则的名字充满疑惑。其实缘由就是这项原则最先是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。
若是对每个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的全部程序 P 在全部的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
全部引用基类的地方必须能透明地使用其子类的对象。
有一功能P1,由类A完成。现须要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会致使原有功能P1发生故障。
当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽可能不要重写父类A的方法,也尽可能不要重载父类A的方法。
继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),其实是在设定一系列的规范和契约,虽然它不强制要求全部的子类必须听从这些契约,可是若是子类对这些非抽象方法任意修改,就会对整个继承体系形成破坏。而里氏替换原则就是表达了这一层含义。
继承做为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。好比使用继承会给程序带来侵入性,程序的可移植性下降,增长了对象间的耦合性,若是一个类被其余的类所继承,则当这个类须要修改时,必须考虑到全部的子类,而且父类修改后,全部涉及到子类的功能都有可能会产生故障。
举例说明继承的风险,咱们须要完成一个两数相减的功能,由类A来负责。
class A{ public int func1(int a, int b){ return a-b; } } public class Client{ public static void main(String[] args){ A a = new A(); System.out.println("100-50="+a.func1(100, 50)); System.out.println("100-80="+a.func1(100, 80)); } }
运行结果:
100-50=50 100-80=20
后来,咱们须要增长一个新的功能:完成两数相加,而后再与100求和,由类B来负责。即类B须要完成两个功能:
因为类A已经实现了第一个功能,因此类B继承类A后,只须要再完成第二个功能就能够了,代码以下:
class B extends A{ public int func1(int a, int b){ return a+b; } public int func2(int a, int b){ return func1(a,b)+100; } } public class Client{ public static void main(String[] args){ B b = new B(); System.out.println("100-50="+b.func1(100, 50)); System.out.println("100-80="+b.func1(100, 80)); System.out.println("100+20+100="+b.func2(100, 20)); } }
类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中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操做,把展示细节的任务交给他们的实现类去完成。
依赖倒置原则的核心思想是面向接口编程,咱们依旧用一个例子来讲明面向接口编程比相对于面向实现编程好在什么地方。场景是这样的,母亲给孩子讲故事,只要给她一本书,她就能够照着书给孩子讲故事了。代码以下:
class Book{ public String getContent(){ return "好久好久之前有一个阿拉伯的故事……"; } } class Mother{ public void narrate(Book book){ System.out.println("妈妈开始讲故事"); System.out.println(book.getContent()); } } public class Client{ public static void main(String[] args){ Mother mother = new Mother(); mother.narrate(new Book()); } }
运行结果:
妈妈开始讲故事 好久好久之前有一个阿拉伯的故事……
运行良好,假若有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码以下:
class Newspaper{ public String getContent(){ return "林书豪38+7领导尼克斯击败湖人……"; } }
这位母亲却办不到,由于她竟然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,竟然必需要修改Mother才能读。假如之后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。缘由就是Mother与Book之间的耦合性过高了,必须下降他们之间的耦合度才行。
咱们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:
interface IReader{ public String getContent(); }
Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改成:
class Newspaper implements IReader { public String getContent(){ return "林书豪17+9助尼克斯击败老鹰……"; } } class Book implements IReader{ public String getContent(){ return "好久好久之前有一个阿拉伯的故事……"; } } class Mother{ public void narrate(IReader reader){ System.out.println("妈妈开始讲故事"); System.out.println(reader.getContent()); } } public class Client{ public static void main(String[] args){ Mother mother = new Mother(); mother.narrate(new Book()); mother.narrate(new Newspaper()); } }
运行结果:
妈妈开始讲故事 好久好久之前有一个阿拉伯的故事…… 妈妈开始讲故事 林书豪17+9助尼克斯击败老鹰……
这样修改后,不管之后怎样扩展Client类,都不须要再修改Mother类了。这只是一个简单的例子,实际状况中,表明高层模块的Mother类将负责完成主要的业务逻辑,一旦须要对它进行修改,引入错误的风险极大。因此遵循依赖倒置原则能够下降类之间的耦合性,提升系统的稳定性,下降修改程序形成的风险。
采用依赖倒置原则给多人并行开发带来了极大的便利,好比上例中,本来Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才能够进行编码,由于Mother类依赖于Book类。修改后的程序则能够同时开工,互不影响,由于Mother与Book类一点关系也没有。参与协做开发的人越多、项目越庞大,采用依赖致使原则的意义就越重大。如今很流行的TDD开发模式就是依赖倒置原则最成功的应用。
传递依赖关系有三种方式,以上的例子中使用的方法是接口传递,另外还有两种传递方式:构造方法传递和setter方法传递,相信用过Spring框架的,对依赖的传递方式必定不会陌生。
在实际编程中,咱们通常须要作到以下3点:
依赖倒置原则的核心就是要咱们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。
客户端不该该依赖它不须要的接口;一个类对另外一个类的依赖应该创建在最小的接口上。
类A经过接口I依赖类B,类C经过接口I依赖类D,若是接口I对于类A和类B来讲不是最小接口,则类B和类D必须去实现他们不须要的方法。
将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们须要的接口创建依赖关系。也就是采用接口隔离原则。
举例来讲明接口隔离原则:
图 1 - 未遵循接口隔离原则的设计
这个图的意思是:类A依赖接口I中的方法一、方法二、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法一、方法四、方法5,类D是对类C依赖的实现。对于类B和类D来讲,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但因为实现了接口I,因此也必需要实现这些用不到的方法。对类图不熟悉的能够参照程序代码来理解,代码以下:
interface I { public void method1(); public void method2(); public void method3(); public void method4(); public void method5(); } class A{ public void depend1(I i){ i.method1(); } public void depend2(I i){ i.method2(); } public void depend3(I i){ i.method3(); } } class B implements I{ public void method1() { System.out.println("类B实现接口I的方法1"); } public void method2() { System.out.println("类B实现接口I的方法2"); } public void method3() { System.out.println("类B实现接口I的方法3"); } //对于类B来讲,method4和method5不是必需的,可是因为接口A中有这两个方法, //因此在实现过程当中即便这两个方法的方法体为空,也要将这两个没有做用的方法进行实现。 public void method4() {} public void method5() {} } class C{ public void depend1(I i){ i.method1(); } public void depend2(I i){ i.method4(); } public void depend3(I i){ i.method5(); } } class D implements I{ public void method1() { System.out.println("类D实现接口I的方法1"); } //对于类D来讲,method2和method3不是必需的,可是因为接口A中有这两个方法, //因此在实现过程当中即便这两个方法的方法体为空,也要将这两个没有做用的方法进行实现。 public void method2() {} public void method3() {} public void method4() { System.out.println("类D实现接口I的方法4"); } public void method5() { System.out.println("类D实现接口I的方法5"); } } public class Client{ public static void main(String[] args){ A a = new A(); a.depend1(new B()); a.depend2(new B()); a.depend3(new B()); C c = new C(); c.depend1(new D()); c.depend2(new D()); c.depend3(new D()); } }
能够看到,若是接口过于臃肿,只要接口中出现的方法,无论对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。若是将这个设计修改成符合接口隔离原则,就必须对接口I进行拆分。在这里咱们将原有的接口I拆分为三个接口,拆分后的设计如图2所示:
图 2 - 遵循接口隔离原则的设计)
照例贴出程序的代码,供不熟悉类图的朋友参考:
interface I1 { public void method1(); } interface I2 { public void method2(); public void method3(); } interface I3 { public void method4(); public void method5(); } class A{ public void depend1(I1 i){ i.method1(); } public void depend2(I2 i){ i.method2(); } public void depend3(I2 i){ i.method3(); } } class B implements I1, I2{ public void method1() { System.out.println("类B实现接口I1的方法1"); } public void method2() { System.out.println("类B实现接口I2的方法2"); } public void method3() { System.out.println("类B实现接口I2的方法3"); } } class C{ public void depend1(I1 i){ i.method1(); } public void depend2(I3 i){ i.method4(); } public void depend3(I3 i){ i.method5(); } } class D implements I1, I3{ public void method1() { System.out.println("类D实现接口I1的方法1"); } public void method4() { System.out.println("类D实现接口I3的方法4"); } public void method5() { System.out.println("类D实现接口I3的方法5"); } }
接口隔离原则的含义是:创建单一接口,不要创建庞大臃肿的接口,尽可能细化接口,接口中的方法尽可能少。也就是说,咱们要为各个类创建专用的接口,而不要试图去创建一个很庞大的接口供全部依赖它的类去调用。本文例子中,将一个庞大的接口变动为3个专用的接口所采用的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的"契约",经过分散定义多个接口,能够预防外来变动的扩散,提升系统的灵活性和可维护性。
说到这里,不少人会觉的接口隔离原则跟以前的单一职责原则很类似,其实否则。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序总体框架的构建。
采用接口隔离原则对接口进行约束时,要注意如下几点:
运用接口隔离原则,必定要适度,接口设计的过大或太小都很差。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
一个对象应该对其余对象保持最少的了解。
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另外一个类的影响也越大。
尽可能下降类与类之间的耦合。
自从咱们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。不管是面向过程编程仍是面向对象编程,只有使各个模块之间的耦合尽可能的低,才能提升代码的复用率。低耦合的优势不言而喻,可是怎么样编程才能作到低耦合呢?那正是迪米特法则要去完成的。
迪米特法则又叫最少知道原则,最先是在1987年由美国Northeastern University的Ian Holland提出。通俗的来说,就是一个类对本身依赖的类知道的越少越好。也就是说,对于被依赖的类来讲,不管逻辑多么复杂,都尽可能地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通讯。首先来解释一下什么是直接的朋友:每一个对象都会与其余对象有耦合关系,只要两个对象之间有耦合关系,咱们就说这两个对象之间是朋友关系。耦合的方式不少,依赖、关联、组合、聚合等。其中,咱们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出如今局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要做为局部变量的形式出如今类的内部。
举一个例子:有一个集团公司,下属单位有分公司和直属部门,如今要求打印出全部下属单位的员工ID。先来看一下违反迪米特法则的设计。
//总公司员工 class Employee{ private String id; public void setId(String id){ this.id = id; } public String getId(){ return id; } } //分公司员工 class SubEmployee{ private String id; public void setId(String id){ this.id = id; } public String getId(){ return id; } } class SubCompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<100; i++){ SubEmployee emp = new SubEmployee(); //为分公司人员按顺序分配一个ID emp.setId("分公司"+i); list.add(emp); } return list; } } class CompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<30; i++){ Employee emp = new Employee(); //为总公司人员按顺序分配一个ID emp.setId("总公司"+i); list.add(emp); } return list; } public void printAllEmployee(SubCompanyManager sub){ List list1 = sub.getAllEmployee(); for(SubEmployee e:list1){ System.out.println(e.getId()); } List list2 = this.getAllEmployee(); for(Employee e:list2){ System.out.println(e.getId()); } } } public class Client{ public static void main(String[] args){ CompanyManager e = new CompanyManager(); e.printAllEmployee(new SubCompanyManager()); } }
如今这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通讯,而SubEmployee类并非CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就好了,与分公司的员工并无任何联系,这样设计显然是增长了没必要要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。修改后的代码以下:
class SubCompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<100; i++){ SubEmployee emp = new SubEmployee(); //为分公司人员按顺序分配一个ID emp.setId("分公司"+i); list.add(emp); } return list; } public void printEmployee(){ List list = this.getAllEmployee(); for(SubEmployee e:list){ System.out.println(e.getId()); } } } class CompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<30; i++){ Employee emp = new Employee(); //为总公司人员按顺序分配一个ID emp.setId("总公司"+i); list.add(emp); } return list; } public void printAllEmployee(SubCompanyManager sub){ sub.printEmployee(); List list2 = this.getAllEmployee(); for(Employee e:list2){ System.out.println(e.getId()); } } }
修改后,为分公司增长了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。
迪米特法则的初衷是下降类之间的耦合,因为每一个类都减小了没必要要的依赖,所以的确能够下降耦合关系。可是凡事都有度,虽然能够避免与非直接的类通讯,可是要通讯,必然会经过一个"中介"来发生联系,例如本例中,总公司就是经过分公司这个"中介"来与分公司的员工发生联系的。过度的使用迪米特原则,会产生大量这样的中介和传递类,致使系统复杂度变大。因此在采用迪米特法则时要反复权衡,既作到结构清晰,又要高内聚低耦合。
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
在软件的生命周期内,由于变化、升级和维护等缘由须要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使咱们不得不对整个功能进行重构,而且须要原有代码通过从新测试。
当软件须要变化时,尽可能经过扩展软件实体的行为来实现变化,而不是经过修改已有的代码来实现变化。
开闭原则是面向对象设计中最基础的设计原则,它指导咱们如何创建稳定灵活的系统。开闭原则多是设计模式六项原则中定义最模糊的一个了,它只告诉咱们对扩展开放,对修改关闭,但是到底如何才能作到对扩展开放,对修改关闭,并无明确的告诉咱们。之前,若是有人告诉我"你进行设计的时候必定要遵照开闭原则",我会觉的他什么都没说,但貌似又什么都说了。由于开闭原则真的太虚了。
在仔细思考以及仔细阅读不少设计模式的文章后,终于对开闭原则有了一点认识。其实,咱们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要咱们对前面5项原则遵照的好了,设计出的软件天然是符合开闭原则的,这个开闭原则更像是前面五项原则遵照程度的"平均得分",前面5项原则遵照的好,平均分天然就高,说明软件设计开闭原则遵照的好;若是前面5项原则遵照的很差,则说明开闭原则遵照的很差。
其实笔者认为,开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。由于抽象灵活性好,适应性广,只要抽象的合理,能够基本保持软件架构的稳定。而软件中易变的细节,咱们用从抽象派生的实现类来进行扩展,当软件须要发生变化时,咱们只须要根据需求从新派生一个实现类来扩展就能够了。固然前提是咱们的抽象要合理,要对需求的变动有前瞻性和预见性才行。
说到这里,再回想一下前面说的5项原则,偏偏是告诉咱们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉咱们实现类要职责单一;里氏替换原则告诉咱们不要破坏继承体系;依赖倒置原则告诉咱们要面向接口编程;接口隔离原则告诉咱们在设计接口的时候要精简单一;迪米特法则告诉咱们要下降耦合。而开闭原则是总纲,他告诉咱们要对扩展开放,对修改关闭。
最后说明一下如何去遵照这六个原则。对这六个原则的遵照并非是和否的问题,而是多和少的问题,也就是说,咱们通常不会说有没有遵照,而是说遵照程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是同样,制定这六个原则的目的并非要咱们刻板的遵照他们,而须要根据实际状况灵活运用。对他们的遵照程度只要在一个合理的范围内,就算是良好的设计。咱们用一幅图来讲明一下。
图中的每一条维度各表明一项原则,咱们依据对这项原则的遵照程度在维度上画一个点,则若是对这项原则遵照的合理的话,这个点应该落在红色的同心圆内部;若是遵照的差,点将会在小圆内部;若是过分遵照,点将会落在大圆外部。一个良好的设计体如今图中,应该是六个顶点都在同心圆中的六边形。
在上图中,设计一、设计2属于良好的设计,他们对六项原则的遵照程度都在合理的范围内;设计三、设计4设计虽然有些不足,但也基本能够接受;设计5则严重不足,对各项原则都没有很好的遵照;而设计6则遵照过渡了,设计5和设计6都是迫切须要重构的设计。
到这里,设计模式的六大原则就写完了。主要参考书籍有《设计模式》《设计模式之禅》《大话设计模式》以及网上一些零散的文章,但主要内容主要仍是我本人对这六个原则的感悟。写出来的目的一方面是对这六项原则系统地整理一下,一方面也与广大的网友分享,由于设计模式对编程人员来讲,的确很是重要。