设计原则就一本菜谱,告诉咱们一道美味的菜应该是什么样的,或者说须要具有什么。可是又没有一个固化或可测量的标准。写代码就和烹饪同样,只有当本身品尝之后才知其味。java
开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽可能在不修改原有代码的状况下进行扩展。编程
开闭原则很简单,就是当需求变动的时候,尽可能不要修改已有代码。开闭原则是整个设计原则的核心思想。若是当你发现某个需求的改动须要涉及许多地方的代码改动,那么你的代码颇有多是不知足开闭原则的。框架
单独说开闭原则是没有什么内容可讲的,就像和你说“坚强的人能够克服任何困难”。那么其实咱们关心的是怎么才能变得“坚强”。其余的设计原则其实都是围绕着怎么实现开闭原则而进行的。this
单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域中的相应职责,或者能够定义为:就一个类而言,应该只有一个引发它变化的缘由。设计
通俗一点说,就是一个类不能承担太多的功能。只专心一件事,而且把这件事作好。举一个例子,假如要写一个食堂类。对于客户端来讲,食堂核心功能是要能打饭和回收餐具。若是要打饭那么确定须要有先把菜作好,若是要作菜还要有人去买菜。那么若是这些功能都放在一个类里面就会以下图:
很明显违反了单一职责原则。为何?明明就是只负责吃相关职能啊。你都说了是吃相关,那么确定就不仅是一个简单功能。吃前要准备,吃后要收拾。其实做为一个服务,就和咱们去食堂吃饭,我只关心怎么打饭,我吃完之后把餐具放哪里。其余的至于菜市从哪里买的,谁炒的都不关心。所以,改良一下
做为食堂,提供多种多样的菜品(如川菜、粤菜)。若是让一个厨师即炒粤菜又炒川菜又违背了单一职责原则。所以再改一下:
单一职责好处就是: 能够下降类的复杂度,提升类的可读性,下降变动引发的风险下降。变动是必然的,若是单一职责原则遵照的好,当修改一个功能时,能够显著下降对其余功能的影响。code
其实单一职责不只仅是针对一个类的设计,往小的说一个方法、往大的说一个模块都应该知足单一职责原则。只是怎么去肯定职责及其范围是须要根据具体的场景来肯定。对象
里氏代换原则(Liskov Substitution Principle, LSP):全部引用基类(父类)的地方必须能透明地使用其子类的对象。继承
里氏代换原则核心就是要知足继承体系,能用父类的地方,那么其任何子类也能够正确调用。不然,就不该该作为其子类(或者父类的定义就不正确)。例如前面的例子中,厨师的子类有川菜厨师、粤菜厨师等,但不管把哪一个厨师放在厨房里面确定都能作菜的。从代码层面上来讲:若是一个类实现一个接口,那么就应该实现该接口的全部方法。比较经典的案例就是Spring的CacheManager的接口。
CacheManager只是提供一个接口,核心功能就是存储Cache。ConcurrentMapCacheManager就是把Cache存在内存Map中,EhCacheCacheManager就是使用EhCache框架。固然若是存在Redis中或者其余存储引擎中,只要实现该接口就能够了。可是就是必须存储cache及其与name的映射关系。接口
若是你理解(或者使用过)面向对象语言,那么里氏替换原则理解起来就十分简单。例如java在编译的时候就会检查一个程序是否符合里氏替换原则。(但编译器只能检查无意的违反原则,但不能检查故意的违反原则)虽然很简单,可是这也是实现开闭原则的基本条件。试想,当扩展一个接口的时候,发现扩展类在实现该接口的方法不能正确调用,那么这个该扩展也是没有任何意义的。图片
依赖倒转原则(Dependency Inversion Principle, DIP):抽象不该该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
上面定义中的抽象能够简单的理解为接口,细节就是接口的实现类。也就是说,不管是定义变量、方法参数类型仍是方法返回都尽可能使用抽象接口,而不是返回具体的实现类。这样作的目的就是方便之后扩展(也就是实现开闭原则的手段)。仍是前面的食堂为例,假如一开始的时候只有MarketA卖菜,采购员就只有去哪里卖菜。也许一开始的代码就会这样写:
class MarketA { public String name() { return ("MarketA"); } } class Buyer { public void buy(MarketA marketA) { System.out.println("采购员在" + marketA.name() + "买菜"); } } class Canteen{ public void buyFood() { Buyer buyer = new Buyer(); buyer.buy(new MarketA()); } }
类图关系以下:
忽然有一天,MarketB开业,而且每周一的价格比MarketA便宜,所以,选择周一在MarketB买菜。
class MarketA { public String name() { return ("MarketA"); } } class MarketB { public String name() { return ("MarketB"); } } class Buyer { public void buyA(MarketA marketA) { System.out.println("采购员在" + marketA.name() + "买菜"); } public void buyB(MarketB marketB) { System.out.println("采购员在" + marketB.name() + "买菜"); } } class Canteen{ public void buyFood() { Buyer buyer = new Buyer(); // 其实这也也不知足单一职责原则,即要选择菜市场,又要派采购员买菜。 if(to is monday){ buyer.buyA(new MarketA()); }else{ buyer.buyB(new MarketB()); } } }
类图:
忽然又有一天,MarketC,MarketD...MarketCX 开业了,而且....好吧,是否是发现这样写下去何时是个头啊。所以咱们须要进行代码重构,使其知足开闭原则。
interface Market { String name(); } class MarketA implements Market { public String name() { return ("MarketA"); } } class MarketB implements Market { public String name() { return ("MarketB"); } } interface Buyer { public void buy(Market market); } //省略Buyer的子类 class Canteen{ public void CanteenBuyFood() { Market market = selectMarket(); Buyer buyer = selectBuyer(); buyer.buy(market); } private Buyer selectBuyer() { //根据实际状况选择采购员 } private Market selectMarket() { // 根据状况选择市场 } }
类图:
重构后代码是否是不管加多少market,只要修改selectMarket()的方法。不管加多少采购员,只要修改Buyer.getBuyer()中的代码。
能够看出,在代码重构过程当中,在大多数状况下开闭原则、里氏代换原则和依赖倒转原则这三个设计原则会同时出现。开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,他们目的都是为了代码的扩展性,只是分析问题时所站角度不一样而已。
接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不该该依赖那些它不须要的接口。
接口隔离就是定义一个接口的时候不要定义得太而广,而是把他们分割成一些更细小的接口。通常不知足单一职责原则都不知足接口隔离原则,这样的例子不少,就很少说明了。但反过来却不必定,举一个例子(每每原则的例子举反例是最清晰的)。仍是买菜的问题,若是咱们定义一个市场,在市场里面买肉、买蔬菜、买水果....),每每一开始咱们定义接口的时候是按照一个样例来定义接口,好比上面就是按照一个超级市场来定义的接口,其实这样作是很正常的状况。
interface Market { void buyMeat(); void buyVegetables(); } class SuperMarket implements Market { public void buyMeat() { System.out.println("买肉"); } public void buyVegetables() { System.out.println("买菜"); } }
这样定义接口在一开始是没问题,可是随着业务扩展,若是如今存在另一个市场,他只卖菜,不卖肉。。那么该类在 buyMeat()下面怎么办?当你发现你实现一个接口的时候,某个须要实现的方法你没办法去实现,这种状况也算不知足接口隔离的原则了。这时候就须要进行重构,把接口进一步细化,例如,把Martet细化为MeatMarket和VegetableMarket。
interface Market { //其余共有方法 } interface MeatMarket extends Market{ void buyMeat(); } interface VegetableMarket extends Market{ void buyVegetables(); } class SuperMarket implements MeatMarket,VegetableMarket { public void buyMeat() { System.out.println("买肉"); } public void buyVegetables() { System.out.println("买菜"); } } class SmallMarket implements VegetableMarket { public void buyVegetables() { System.out.println("买菜"); } }
类图:
上面的实例代码仅仅是为了说明接口隔离原则,
接口隔离原则核心思想就是细化接口,提升程序的灵活性。但细化到什么程度却没有具体的度量,接口不能过小,若是过小会致使系统中接口泛滥,反而不利于维护。所以如何把握这个读就是经验了。
迪米特法则(Law of Demeter, LoD):一个软件实体应当尽量少地与其余实体发生相互做用。
迪米特法则还有几种定义形式,包括:不要和“陌生人”说话、只与你的直接朋友通讯等
所谓一个类的朋友,就是该类
从代码层面上讲,当该类的全部方法体为空的时候,该类所依赖的类就是朋友类 所谓只与他朋友类进行交互意思就比较好理解了,就是不能直接调用他朋友的朋友方法。也就是在下图中:
食堂服务==朋友==>食堂后勤==朋友==>厨师,咱们不能直接在食堂服务中直接调用让厨师去炒菜。食堂服务类要与厨师类通讯,也是必需要有食堂后勤类做为中介。从代码层面上来讲,通常 A.getB().getC().xxxMethod() 每每都是破坏迪米特法则。
一个类公开的public属性或方法越多,修改时涉及的面也就越大,变动引发的风险扩散也就越大。例如,
class A { public void step1(){ //do something } public void step2(){ //do something} } public void step3(){ //do something} } } class M { public void someCall(A a) { a.step1(); a.step2(); a.step3(); } }
从代码里面看,A确实是M的朋友,可是,M和A太"亲密"了,若是之后须要在调用step2的以前调用一个check2()的方法,就必需要修改M中的方法,若是这个调用方式大量出如今工程中,就会引发扩散。若是我以前是这么设计的交互的方式就不会存在这个问题。也就是说,朋友之间的交互不要太"亲密"。同时做为别人朋友,最好能提供“一站式服务”。
class A { private void step1(){ //do something } private void step2(){ //do something} } private void step3(){ //do something} } public void exe(){ step1(); step2(); step3(); } } class M { public void someCall(A a) { a.exe(); } }
迪米特发展的目的就是下降系统的耦合度,使类与类之间保持松散的耦合关系。和接口隔离原则同样,迪米特法则也是一个没法进行度量。过分使用迪米特法则,也会形成系统的不一样模块之间的通讯效率下降,这就直接致使了系统中存在大量的中介类。所以,如何使用迪米特法则仍是那句话,根据经验。
单一职责原则告诉咱们实现类的功能不要太多。里氏替换原则告诉咱们不要破坏继承体系;依赖倒置原则告诉咱们要面向接口编程;接口隔离原则告诉咱们在设计接口的时候要精简单一;迪米特法则告诉咱们要下降耦合。而开闭原则总的大纲,要对扩展开放,对修改关闭。 设计原则只是给咱们一些指导性的意见,在实际工做中每每要根据实际状况来判断。就如开篇所说的那样,每每菜谱每每给咱们的意见都是“少量”,“适量”,而真正要烹饪出一道美味还须要咱们本身不断去积累和调整。