单一职责原则(Single responsibility principle,SRP)

定义:一个类只负责一项职责,不要存在多于一个导致类变更的原因。

原因职责扩散。因为某种原因,职责R被分成了更细粒度的职责R1和R2。比如,有一个类T负责两个不同的职责,职责R1和职责R2,当职责R1需求发生变化的时候需要修改类T,此时就有风险会使得职责R2发生故障。

场景:类或者方法比较复杂,职责扩散不可避免,在扩散超出我们控制范围之前,立即使用单一职责原则对代码进行重构。

所以说,单一职责原则不是必须的,而是要看具体的场景。下面举例说明。

代码实例:

public class Animal {public void breath(String animal){System.out.println(animal + " breath air!");}}
public class Main {public static void main(String[] args) {Animal animal = new Animal();animal.breath("sheep");animal.breath("pig");animal.breath("cow");}}

如上代码就是符合单一职责原则的例子。Animal类只负责呼吸只一个职责。

现在出于某种原因,职责发生了扩散,比如我们发现不是所有Animal都是呼吸空气的,像鱼就是呼吸水的,那么最简单的就是在Animal中增加一个判断逻辑。

public class Animal2 {public void breath(String animal){if("fish".equals(animal)){System.out.println(animal + " breath water!");}else {System.out.println(animal + " breath air!");}}}
public class Main {public static void main(String[] args) {Animal2 animal = new Animal2();animal.breath("sheep");animal.breath("pig");animal.breath("cow");animal.breath("fish");}
}

但是很明显,它不符合单一职责原则,对后续的维护十分不便利。如果按照单一职责的话,那么应该修改如下:

public class Aquatic {//水生生物public void breath(String animal){System.out.println(animal + " breath water!");}}
public class Terrestrial {//陆生生物public void breath(String animal){System.out.println(animal + " breath air!");}}
public class Main {public static void main(String[] args) {Terrestrial t = new Terrestrial();t.breath("sheep");t.breath("pig");t.breath("cow");Aquatic a = new Aquatic();a.breath("fish");}}

可以看到,这样修改处理需要将原有的Animal类进行分解之外,还需要修改客户端。还可以用下这种修改方式。

public class Animal3 {public void aquaticBreath(String animal){//水生动物呼吸System.out.println(animal + " breath water!");}public void terrestrialBreath(String animal){陆生动物呼吸System.out.println(animal + " breath air!");}}
public class Main {public static void main(String[] args) {Animal3 animal = new Animal3();animal.terrestrialBreath("sheep");animal.terrestrialBreath("pig");animal.terrestrialBreath("cow");animal.aquaticBreath("fish");}}

如上所示,水生和陆生的动物分别调用不同的方法,虽然在类级别上违反了单一职责原则,但是在方法层面上确是符合单一职责的,即一个方法只负责一个职责。

单一职责的优点

  • 可以降低类的复杂度,一个类只负责一项职责,逻辑会比较清晰简单。
  • 提高类的可读性,使系统具有很高的可维护性。
  • 降低变更逻辑时的风险,变更时必然的。

总结
并不是说写任何代码一定要符合单一职责原则,我们在上面总共列出三种代码,分别是if-else的代码、类级别符合单一职责的代码、方法级别符合单一职责的代码。这三种各有各的好处,一般使用场景应该参考如下要点:

  • 当逻辑足够简单,后续不太可能会再次发生职责扩散,那么可以考虑使用if-else这种的代码方式,毕竟最容易修改,最快速完成。
  • 当类中的方法数量足够的少,逻辑简单的完全处于掌控之中的时候,可以在方法级别上满足单一职责,这样毕竟不用新建类,不用大规模地分离代码。
  • 当逻辑快要超出你掌控范围了,赶快进行类级别的单一职责重构吧。否则,你每一次的修改增加都有很大风险会影响其它功能。

里氏替换原则(Liskov Substitution Principle,LSP)

定义:所有引用基类的地方必须能透明地使用其子类的对象,并且程序的行为不会发生变化。

原因:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P2由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

使用:类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

里氏替换原则主要是为了解决继承的问题,父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改(重写或者重载),就会对整个继承体系造成破坏。
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。

代码实例:

public class Calculator {public int operation(int a,int b){return a-b;}}
public class Main {public static void main(String[] args) {Calculator c = new Calculator();System.out.println("100-50=" + c.operation(100, 50));}}

如上是实现一个计算器,但是只有减法功能。现在需要扩展该计算器,增加一个自定义计算。

public class SelfDefitionCalculator extends Calculator {public int operation(int a,int b){return a*b;}public int cal(int a,int b){return operation(a,b) + 100;}}
public class Main {public static void main(String[] args) {SelfDefitionCalculator sdc = new SelfDefitionCalculator();//想使用父类的减法功能System.out.println("100-50=" + sdc.operation(100, 50));//使用自定义操作System.out.println("100*50+100=" + sdc.cal(100, 50));}}

运行后结果为:

100-50=5000
100*50+100=5100

我们发现原本运行正常的相减功能发生了错误。原因就是子类在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了重写后的方法,造成原本运行正常的功能出现了错误。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。

依赖倒置原则(Dependency Inversion Principle,DIP)

定义:高层模块不应该依赖于底层模块,它们都应该依赖抽象;抽象不依赖细节,细节依赖抽象。

原因:类A是高层模块,负责实现具体的业务逻辑,类B是底层模块,负责原子操作。类A依赖类B,当需求发生变化,需要将类A改为依赖类C,那么必须修改类A的代码,这会给程序带来很大的风险。  

使用:给底层模块的类B和类C抽象出一个接口I(或者抽象类),类A依赖接口I,这样,当需要改变依赖对象的时候,就降低了修改类A的几率。

代码实例:

public class Book {public String getContent(){return "Book Content Start!";}}
public class Mother {public void tellStory(Book book){System.out.println(book.getContent());}}
public class Main {public static void main(String[] args) {Mother mother = new Mother();mother.tellStory(new Book());}}

在如上的代码中,高层模块Mother类直接依赖具体的实现类Book,违反了依赖倒置原则。当不再读书上的故事,而是读报纸、杂志、网络上的故事的时候,需要将Mother类中的Book改成对应的底层模块类,不仅为Mother类的正常运行引进了风险,而且不利于维护。
现在依据依赖倒置原则进行重构,高层和底层模块都依赖抽象。

public interface IReader {public String getContent();}
public class Magazine implements IReader {@Overridepublic String getContent() {return "Magazine Content Start!";}}
public class Newspaper implements IReader {@Overridepublic String getContent() {return "Newspaper Content Start!";}}
public class Mother2 {public void tellStory(IReader reader){System.out.println(reader.getContent());}}
public class Main {public static void main(String[] args) {Mother2 mother = new Mother2();mother.tellStory(new Magazine());}}

在重构后的代码中可以看到,Mother2类依赖的是抽象IReader接口,以后需要读别的类型的故事,就不再需要修改Mother2类了。
依赖倒置原则的核心就是面向接口编程,在实际编程中,一般遵循以下几点:

  • 低层模块尽量都要有抽象类或接口,或者两者都有。
  • 变量的声明类型尽量是抽象类或接口。
  • 使用继承时遵循里氏替换原则。

接口隔离原则(Interface Segregation Principle,ISP)

定义:类不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。

原因:一个综合类的接口包含了很多的抽象方法,类A在实现这个接口的时候,不得不实现很多它其实不需要的方法。

代码实例:

public interface Creature {public void makeSound();public void drink();public void breath();public void photosynthesis();}
public class Animal implements Creature {@Overridepublic void makeSound() {System.out.println("Animal make sound");}@Overridepublic void drink() {System.out.println("Animal drink water");}@Overridepublic void breath() {System.out.println("Animal breath air");}@Overridepublic void photosynthesis() {System.out.println("Animal can not photosynthesis");}}
public class Plants implements Creature {@Overridepublic void makeSound() {System.out.println("Plants can not make sound");}@Overridepublic void drink() {System.out.println("Plants drink water");}@Overridepublic void breath() {System.out.println("Plants breath air");}@Overridepublic void photosynthesis() {System.out.println("Plants photosynthesis");}}
public class Main {public static void main(String[] args) {Creature sheep = new Animal();sheep.breath();sheep.drink();sheep.makeSound();Creature appleTree = new Plants();appleTree.breath();appleTree.drink();appleTree.photosynthesis();//unexpected behaviorsSystem.out.println("Below are unexpected behaviors:");sheep.photosynthesis();appleTree.makeSound();}}

在如上的代码中,接口Creature包含了生物的很多特性方法。比如makeSound(发声)、drink(饮水)、breath(呼吸)、photosynthesis(光合作用)。它的实现类Animal和Plants并不是都需要所有这些方法,比如Animal不需要光合作用,Plants不需要发声。如此,在Animal和Plants中不得不实现它们不需要的方法,最后还可能引起在客户端中发生意想不到的行为,比如动物进行光合作用,这显然是很不安全的。

解决:为了符合接口隔离原则,必须对以上的Creature接口进行拆分,我们将所有的生物行为拆到三个接口中:

public interface Creature2 {public void drink();public void breath();}
public interface AnimalCreature {public void makeSound();}
public interface PlantsCreature {public void photosynthesis();}
public class Animal2 implements Creature2, AnimalCreature {@Overridepublic void makeSound() {System.out.println("Animal make sound");}@Overridepublic void drink() {System.out.println("Animal drink water");}@Overridepublic void breath() {System.out.println("Animal breath air");}}
public class Plants2 implements Creature2, PlantsCreature {@Overridepublic void photosynthesis() {System.out.println("Plants photosynthesis");}@Overridepublic void drink() {System.out.println("Plants drink water");}@Overridepublic void breath() {System.out.println("Plants breath air");}}

其中接口Creature2值存放公共的方法,AnimalCreature和PlantsCreature分别存放各自需要的方法。如此,Animal2和Plants2在实现的时候只需要实现自己需要的即可。

接口隔离原则的含义是:
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

注意点

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。

迪米特法则(Law of Demeter,LoD;Least Knowledge Principle,LKP)

定义:一个对象应该对其他对象保持最少的了解。

原因:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

注意:迪米特法则又叫最少知道原则,一个类对自己依赖的类应该知道的越少越好;对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。
迪米特法则还有一个更简单的定义:只与直接的朋友通信。那么符合什么样的条件才算是直接的朋友呢?

  • 当前对象本身(this)。
  • 以参量形式传入到当前对象方法中的对象。
  • 当前对象的实例变量直接引用的对象。
  • 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友。
  • 当前对象所创建的对象。

任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”;否则就是“陌生人”。

代码实例:

public class Animals {private String category;public String getCategory() {return category;}public void setCategory(String category) {this.category = category;}}
public class Humans {private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}}
public class Forests {private List<Animals> list;public void generateAnimals(){list = new ArrayList<Animals>();for(int i=0;i<5;i++){Animals a = new Animals();a.setCategory("category" + i);list.add(a);}}public List<Animals> getAllAnimals(){return list;}}
public class Earth {private List<Humans> list;public void generateHumans(){list = new ArrayList<Humans>();for(int i=0;i<3;i++){Humans a = new Humans();a.setName("name" + i);list.add(a);}}public List<Humans> getAllHumans(){return list;}public void printAllCreatures(Forests f){f.generateAnimals();List<Animals> animalsList = f.getAllAnimals();for (Animals animals : animalsList) {System.out.println(animals.getCategory());}this.generateHumans();List<Humans> humansList = this.getAllHumans();for (Humans humans : humansList) {System.out.println(humans.getName());}}}
public class Main {public static void main(String[] args) {Earth earth = new Earth();Forests forest = new Forests();earth.printAllCreatures(forest);}}

在以上的代码中,类Earth中的List、Humans、Forests所创建的对象都是直接的朋友,而List、Animals所创建的对象都不是直接的朋友(具体参考上面判断直接朋友的方法),这违反了迪米特法则。

从逻辑上看Animals的行为也不应该由Earth来负责,完全可以交给Forests来处理,所以,Earth和Animals的耦合其实是不必要的。

修改后的代码:

public class Forests2 {private List<Animals> list;public void generateAnimals(){list = new ArrayList<Animals>();for(int i=0;i<5;i++){Animals a = new Animals();a.setCategory("category" + i);list.add(a);}}public List<Animals> getAllAnimals(){return list;}public void printAllAnimals(){this.generateAnimals();List<Animals> animalsList = this.getAllAnimals();for (Animals animals : animalsList) {System.out.println(animals.getCategory());}}}
public class Earth2 {private List<Humans> list;public void generateHumans(){list = new ArrayList<Humans>();for(int i=0;i<3;i++){Humans a = new Humans();a.setName("name" + i);list.add(a);}}public List<Humans> getAllHumans(){return list;}public void printAllCreatures(Forests2 f){f.printAllAnimals();this.generateHumans();List<Humans> humansList = this.getAllHumans();for (Humans humans : humansList) {System.out.println(humans.getName());}}}
public class Main {public static void main(String[] args) {Earth2 earth = new Earth2();Forests2 forest = new Forests2();earth.printAllCreatures(forest);}}

更改过后,Earth2中的对象都是直接朋友,非直接朋友Animals被移到了Forests2中,成了Forests2的直接朋友。如此实现了解耦,下次如果修改Animals类,就不需要修改Earth2了。

迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

开闭原则(Open Closed Principle,OCP)

定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

原因:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

解决:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

其实,我们遵循设计模式前面5大原则的目的就是遵循开闭原则,也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的。

开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性预见性才行。

回想一下前面说的5项原则:

  • 单一职责原则告诉我们实现类要职责单一;
  • 里氏替换原则告诉我们不要破坏继承体系;
  • 依赖倒置原则告诉我们要面向接口编程;
  • 接口隔离原则告诉我们在设计接口的时候要精简单一;
  • 迪米特法则告诉我们要降低耦合;
  • 开闭原则是总纲,对扩展开放,对修改关闭。

代码:可以参考前面依赖倒置原则的代码,这就是符合开闭原则的一个例子。还有后面将要讲的策略模式,也是遵循的开闭原则,这里不再赘述。

总结

对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。

参考:http://blog.csdn.net/zhengzhb/article/category/926691/1

设计模式的六大原则(快速复习版)相关推荐

  1. 【设计模式 00】设计模式的六大原则

    设计模式的六大原则 参考: 设计模式六大原则 1. 单一职责原则 一个类只负责一个明确的功能 优点: 降低类的复杂度,提高代码可读性和可维护性 降低变更时对其他功能的影响 2. 里氏替换原则 **原则 ...

  2. 神了!有人用一个项目把23种设计模式与六大原则融会贯通了

    前言 设计模式分为三类,创建型,结构型和行为型.创建型比较好理解,它抽象了实例化过程,将系统与实例的创建解耦.实例由专门的工厂来创建,从而使系统针对实例的抽象接口编程,不依赖任何具体的实现.结构型和行 ...

  3. 如何理解设计模式的六大原则?

    我们知道,java中有类,父类,抽象类,接口这些概念,而设计模式的六大原则就解释了它们及其它们之间的关系是怎样的,接下来我们将由简到难一一剖析. 开闭原则:是总纲,他告诉我们要对扩展开放,对修改关闭. ...

  4. 设计模式的六大原则(SOLID)

    设计模式的六大原则有: Single Responsibility Principle:单一职责原则 Open Closed Principle:开闭原则 Liskov Substitution Pr ...

  5. unity 项目开发——浅谈设计模式的六大原则(一)

    目录 前言 首先,六大原则是谁? 其次,为什么需要学习这六大原则? 正文 一.单一职责原则 示例: 因此我们需要进行拆分,根据具体的职能可将其具体拆分如下: Unity 单一职责原则 二.开闭原则 U ...

  6. 设计模式的六大原则。简称:SOLID

    设计模式的六大原则.简称:SOLID 迪米特法则(Law of Demeter):如果两个类之间并不需要直接通信,那么就不应该让这两个类相互作用.又叫作最少知识原则 迪米特法则的定义是:只与你的直接朋 ...

  7. 「设计模式」六大原则之三:里氏替换原则小结

    文章目录 1.里式替换原则定义 2. 举例说明 示例1: 示例2: 3. 哪些代码明显违背了 LSP? 子类违背父类声明要实现的功能 子类违背父类对输入.输出.异常的约定 子类违背父类注释中所罗列的任 ...

  8. (转)设计模式之六大原则

    源地址:http://blog.csdn.net/scboyhj__/article/details/47844639 1. 单一职责原则 -Single Responsibility Princip ...

  9. 【设计模式】——六大原则

    ◎原则之间的关系◎ 开闭原则是目标,里氏转换原则是基础,依赖倒转原则是手段.六大原则追求的目标都是,可扩展,复用性强,灵活性高,容易维护,耦合度低. ★★单一职责原则★♬★ 定义:就一个类而言,应该仅 ...

  10. 何红辉设计模式之六大原则

    这篇文章总结的很到位: https://www.jianshu.com/p/807bc228dbc2 欢迎去阅读 设计模式六大原则 ,简称:solid 1.Single Responsibility ...

最新文章

  1. appium+python自动化45-夜神模拟器连不上(adb server version (36) doesn't match this client (39); killing...)...
  2. ecshop商城首页怎样设置广告ALT标签
  3. 企业网络推广——企业网络推广表示合理采集可助力网站品质提升
  4. MATLAB从入门到精通-Simulink模块连续模块之积分(Continuous-Integrator)
  5. 如何循序渐进向DotNet架构师发展
  6. vue商城项目开发:封装banner组件、组件参数传递
  7. android 代码签名apk,[Android]混淆代码后生成带签名的apk
  8. 《南溪的目标检测学习笔记》——性能优化的学习笔记
  9. 玩转ptrace (一)
  10. 基于jQuery鼠标悬停上下滑动导航条
  11. vue本地静态图片的路径问题解决方案
  12. H3C IPsec穿越nat实验
  13. 2022电工杯:5G 网络环境下应急物资配送问题(优化)
  14. 【日常小问题3】win10电脑忘记开机密码的解锁方法【转载】
  15. java面试之Servlet/JSP面试题
  16. Visio连接线的箭头如何变为直线、双箭头;直线转换为箭头
  17. 微信翻译生日快乐的代码_新套路,微信这个翻译功能还能帮你表白,快学起来!...
  18. 小样本学习记录————利用所有数据的元学习Few-shot Text Classification with Distributional Signatures
  19. 谷歌Fuchsia操作系统对编程语言的支持决议
  20. android相关学习网站

热门文章

  1. python爬取一张图片并保存_python爬取网页图片并保存到本地
  2. 深度有趣 | 08 DCGAN人脸图片生成
  3. 智力题汇总(思维+速度)
  4. GitHub+Hexo搭建自己的Blog之(3)-主题配置(Next)
  5. Python使用OpenCV拼接图片
  6. 大数据:变革世界的关键资源
  7. 临时停车收费管理系统的设计与实现
  8. 通达信公式编程:反包及其原理
  9. matlab pcode 单步运行,[转载]Matlab中的profile工具
  10. 电脑截图,电脑怎么截图,教您电脑怎么截屏