1. 单一职责原则 -Single Responsibility Principle

SRP,Single Responsibility Principle:

There should never be more than one reason for a class to change.

应该有且仅有一个原因引起类的变更。(如果类需要变更,那么只可能仅由某一个原因引起)

问题由来:

类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。

解决方案:

遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

示例:

如果一个接口包含了两个职责,并且这两个职责的变化不互相影响,那么就可以考虑拆分成两个接口。

方法的职责应清晰、单一。一个method尽可能制作一件事情。changeUserInfo()可拆分为changeUserName()、changeUserAddr()....

说到单一职责原则,很多人都会不屑一顾。因为它太简单了。稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,因为这是常识。在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。而避免出现这一问题的方法便是遵循单一职责原则。虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。

为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。此时,按照SRP 应该再新建一个类负责职责P2,但是这样会修改花销很大!除了改接口 还需要改客户端代码!所以一般就直接在原有类方法中增加判断 支持职责P2;或者在原有类中新增一个方法来处理职责P2(做到了方法级别的SRP),

例如原有一个接口,模拟动物呼吸的场景:

  1. class Animal{
  2. public void breathe(String animal){
  3. System.out.println(animal+"呼吸空气");
  4. }
  5. }
  6. public class Client{
  7. public static void main(String[] args){
  8. Animal animal = new Animal();
  9. animal.breathe("牛");
  10. animal.breathe("羊");
  11. animal.breathe("猪");
  12. }
  13. }

程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。

修改一:修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码如下:

  1. class Terrestrial{
  2. public void breathe(String animal){
  3. System.out.println(animal+"呼吸空气");
  4. }
  5. }
  6. class Aquatic{
  7. public void breathe(String animal){
  8. System.out.println(animal+"呼吸水");
  9. }
  10. }
  11. public class Client{
  12. public static void main(String[] args){
  13. <strong>Terrestrial </strong>terrestrial = new Terrestrial();
  14. terrestrial.breathe("牛");
  15. terrestrial.breathe("羊");
  16. terrestrial.breathe("猪");
  17. <strong>Aquatic </strong>aquatic = new Aquatic();
  18. aquatic.breathe("鱼");
  19. }
  20. }

BUT,这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。

修改二:直接修改类Animal;虽然违背了单一职责原则,但花销却小的多

  1. class Animal{
  2. public void breathe(String animal){
  3. if("鱼".equals(animal)){
  4. System.out.println(animal+"呼吸水");
  5. }else{
  6. System.out.println(animal+"呼吸空气");
  7. }
  8. }
  9. }
  10. public class Client{
  11. public static void main(String[] args){
  12. Animal animal = new Animal();
  13. animal.breathe("牛");
  14. animal.breathe("羊");
  15. animal.breathe("猪");
  16. animal.breathe("鱼");
  17. }
  18. }

这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。

这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。

修改三:

  1. class Animal{
  2. public void breathe(String animal){
  3. System.out.println(animal+"呼吸空气");
  4. }
  5. public void breathe2(String animal){
  6. System.out.println(animal+"呼吸水");
  7. }
  8. }
  9. public class Client{
  10. public static void main(String[] args){
  11. Animal animal = new Animal();
  12. animal.breathe("牛");
  13. animal.breathe("羊");
  14. animal.breathe("猪");
  15. animal.breathe2("鱼");
  16. }
  17. }

这种修改方式没有改动原来的方法,而是在类中新加了一个方法;虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。

好处:

一个接口的修改只对相应的实现类有影响,对其他接口无影响;有利于系统的可扩展性、可维护性。

问题:

“职责”的粒度不好确定!

过分细分的职责也会人为地增加系统复杂性。

建议:

对于单一职责原则,建议 接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;

2. 里氏替换原则 -Liskov Substitution Principle

LSP,Liskov Substitution Principle:

1) If for each object s of type S, there is an objectt of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when s is substituted fort when S is a subtype of T.

2) Functions that use pointers or references to base classes must be able to user objects of derived classes without knowing it.

所有引用基类的地方,都能透明地替换成其子类对象。只要父类能出现的地方,子类就可以出现。

引入里氏替换原则能充分发挥继承的优点、减少继承的弊端。

继承的优点:

  • 代码共享,减少创建类的工作量;每个子类都有父类的方法和属性;
  • 提高代码重用性;
  • 子类可以形似父类,但又异于父类;
  • 提高代码可扩展性;
  • 提高产品开放性。

继承的缺点:

  • 继承是侵入性的——只要继承,就必须拥有父类的属性和方法;
  • 降低代码的灵活性——子类必须拥有父类的属性和方法,让子类自由的世界多了些约束;
  • 增强了耦合性——当父类的属性和方法被修改时,必须要考虑子类的修改。


示例(继承的缺点):

原有类A,实现减法功能:

  1. class A{
  2. public int func1(int a, int b){
  3. return a-b;
  4. }
  5. }
  6. public class Client{
  7. public static void main(String[] args){
  8. A a = new A();
  9. System.out.println("100-50="+a.func1(100, 50));
  10. System.out.println("100-80="+a.func1(100, 80));
  11. }
  12. }

新增需求:新增两数相加、然后再与100求和的功能,由类B来负责

  1. class B extends A{
  2. public int func1(int a, int b){
  3. return a+b;
  4. }
  5. public int func2(int a, int b){
  6. return func1(a,b)+100;
  7. }
  8. }
  9. public class Client{
  10. public static void main(String[] args){
  11. B b = new B();
  12. System.out.println("100-50="+b.func1(100, 50));
  13. System.out.println("100-80="+b.func1(100, 80));
  14. System.out.println("100+20+100="+b.func2(100, 20));
  15. }
  16. }

OOPS! 原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法!

问题由来:

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

解决方案:

LSP为继承定义了一个规范,包括四层含义:

        1)子类必须完全实现父类的方法

如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生畸变;则建议不要用继承,而采用依赖、聚集、组合等关系代替继承。

例如:父类AbstractGun有shoot()方法,其子类ToyGun不能完整实现父类的方法(玩具枪不能射击,ToyGun.shoot()中没有任何处理逻辑),则应该断开继承关系,另外建一个AbstractToy父类。

        2)子类可以有自己得个性

即,在子类出现的地方,父类未必就能替代。


        3)重载或实现父类方法时,输入参数可以被放大(入参可以更宽松)

否则,用子类替换父类后,会变成执行子类重载后的方法,而该方法可能“歪曲”父类的意图,可能引起业务逻辑混乱。

        4)重写或实现父类方法时,返回类型可以被缩小(返回类型更严格)

建议:

在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。

父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

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

Q:

LSP如何减少继承的弊端?

3. 依赖倒置原则 -Dependence Inversion Principle:

DIP,Dependence Inversion Principle:

High level modules should not depend upon low level modules. Both should depend upon abstractions.

Abstractions should not depend upon details. Details should depend upon abstractions.

“面向接口编程”

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;——模块间的依赖通过抽象发生。实现类之间不发生直接的依赖关系(eg. 类B被用作类A的方法中的参数),其依赖关系是通过接口或抽象类产生的;
  • 抽象不应该依赖细节;——接口或抽象类不依赖于实现类;
  • 细节应该依赖抽象;——实现类依赖接口或抽象类。

何为“倒置”?

依赖正置:类间的依赖是实实在在的实现类间的依赖,即面向实现编程,这是正常人的思维方式;

而依赖倒置是对现实世界进行抽象,产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖。

依赖倒置可以减少类间的耦合性、降低并行开发引起的风险。

示例(减少类间的耦合性):

例如有一个Driver,可以驾驶Benz:

  1. public class Driver{
  2. public void drive(Benz benz){
  3. benz.run();
  4. }
  5. }
  6. public class Benz{
  7. public void run(){
  8. System.out.println("Benz开动...");
  9. }
  10. }

问题来了:现在有变更,Driver不仅要驾驶Benz,还需要驾驶BMW,怎么办?

Driver和Benz是紧耦合的,导致可维护性大大降低、稳定性大大降低(增加一个车就需要修改Driver,Driver是不稳定的)。

示例(降低并行开发风险性):

如上例,Benz类没开发完成前,Driver是不能编译的!不能并行开发!

问题由来:

类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

解决办法:

将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

上例中,新增一个抽象ICar接口,ICar不依赖于BMW和Benz两个实现类(抽象不依赖于细节)。

1)Driver和ICar实现类松耦合

2)接口定下来,Driver和BMW就可独立开发了,并可独立地进行单元测试

依赖有三种写法:

      1)构造函数传递依赖对象(构造函数注入)

  1. public interface IDriver{
  2. public void drive();
  3. }
  4. public class Driver implements IDriver{
  5. private ICar car;
  6. public <strong>Driver(ICar _car)</strong>{
  7. this.car = _car;
  8. }
  9. public void drive(){
  10. this.car.run();
  11. }
  12. }

      2)setter方法传递依赖对象(setter依赖注入)

  1. public interface IDriver{
  2. public void setCar(ICar car);
  3. public void drive();
  4. }
  5. public class Driver implements IDriver{
  6. private ICar car;
  7. public void <strong>setCar(ICar car)</strong>{
  8. this.car = car;
  9. }
  10. public void drive(){
  11. this.car.run();
  12. }
  13. }

      3)接口声明依赖对象(接口注入)

建议:

DIP的核心是面向接口编程;DIP的本质是通过抽象(接口、抽象类)使各个类或模块的实现彼此独立,不互相影响。

在项目中遵循以下原则:

  1. 每个类尽量都有接口或抽象类
  2. 变量的表面类型尽量使接口或抽象类
  3. 任何类都不应该从具体类派生*——否则就会依赖具体类。
  4. 尽量不要重写父类中已实现的方法——否则父类就不是一个真正适合被继承的抽象。
  5. 结合里氏替代原则使用

4. 接口隔离原则 -Interface Segregation Principle

Interface Segregation Principle:

Clients should not be forced to depend upon interfaces that they don't use.——客户端只依赖于它所需要的接口;它需要什么接口就提供什么接口,把不需要的接口剔除掉。

The dependency of one class to another one should depend on the smallest possible interface.——类间的依赖关系应建立在最小的接口上。

即,接口尽量细化,接口中的方法尽量少

问题由来:

类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法

解决方案:

将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。包含4层含义:

      1)接口要尽量小

不能出现Fat Interface;但是要有限度,首先不能违反单一职责原则(不能一个接口对应半个职责)。

        2)接口要高内聚

在接口中尽量少公布public方法。

接口是对外的承诺,承诺越少对系统的开发越有利。

       3)定制服务

只提供访问者需要的方法。例如,为管理员提供IComplexSearcher接口,为公网提供ISimpleSearcher接口。

       4)接口的设计是有限度的

建议:

  • 一个接口只服务于一个子模块或业务逻辑;
  • 通过业务逻辑压缩接口中的public方法;
  • 已被污染了的接口,尽量去修改;若变更的风险较大,则采用适配器模式转化处理;
  • 拒绝盲从


与单一职责原则的区别:

二者审视角度不同;

单一职责原则要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分;

接口隔离原则要求接口的方法尽量少。。。

5. 迪米特法则 -Law of Demeter

LoD,Law of Demeter:

又称最少知识原则(Least Knowledge Principle),一个对象应该对其他对象有最少的了解

一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。

问题由来:

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

解决方案:

迪米特法则包含4层含义:

        1)只和朋友交流

Only talk to your immediate friends.两个对象之间的耦合就成为朋友关系。即,出现在成员变量、方法输入输出参数中的类就是朋友;局部变量不属于朋友。

--> 不与无关的对象发生耦合!

        方针:不要调用从另一个方法中返回的对象中的方法!只应该调用以下方法:

  • 该对象本身的方法
  • 该对象中的任何组件的方法
  • 方法参数中传入的对象的方法
  • 方法内部实例化的对象的方法

例如:Teacher类可以命令TeamLeader对Students进行清点,则Teacher无需和Students耦合,只需和TeamLeader耦合即可。

反例:

  1. public float getTemp(){
  2. Thermometer t = station.getThermometer();
  3. return t.getTemp();
  4. }

客户端不应该了解气象站类中的温度计对象;应在气象站类中直接加入获取温度的方法。改为:

  1. public float getTemp(){
  2. return station.getTemp();
  3. }

2)朋友间也应该有距离

即使是朋友类之间也不能无话不说,无所不知。

--> 一个类公开的public属性或方法应该尽可能少!

     3)是自己的就是自己的

如果一个方法放在本类中也可以、放在其他类中也可以,怎么办?

--> 如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中。

        4)谨慎使用Serializable

否则,若后来修改了属性,序列化时会抛异常NotSerializableException。

建议:

迪米特法则的核心观念是:类间解耦。

其结果是产生了大量中转或跳转类。

6. 开闭原则 -Open Closed Principle

Open Closed Principle:

Software entities like classes, modules and functions should be open for extension but closed for modifications.

对扩展开放,对修改关闭。一个软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。

一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。——but,并不意味着不做任何修改;底层模块的扩展,必然要有高层模块进行耦合。

“变化”可分为三种类型:

  1. 逻辑变化——不涉及其他模块,可修改原有类中的方法;
  2. 子模块变化——会对其他模块产生影响,通过扩展来完成变化;
  3. 可见视图变化——界面变化,有时需要通过扩展来完成变化。


问题由来:

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

解决方案:

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

       1)抽象约束(要实现对扩展开放,首要前提就是抽象约束)

通过接口或抽象类可以约束一组可能变化的行为,并能实现对扩展开放。包含三层含义:

  • 通过接口或抽象类可以约束扩展,对扩展进行边界限定,不允许出现在接口抽象类中不存在的public方法;
  • 参数类型、引用对象尽量使用接口或抽象类,而不是实现类;
  • 抽象应尽量保持稳定,一旦确定即不允许修改。

        2)元数据(metadata)控制模块行为

元数据,即用来描述环境和数据的数据,即配置数据。例如SpingContext。

      3)制定项目章程

      4)封装变化

封装可能发生的变化。将相同的变化封装到一个接口或抽象类中;将不同的变化封装到不同的接口或抽象类中。

好处:

  • 易于单元测试

如果直接修改已有代码,则需要同时修改单元测试类;而通过扩展,则只需生成一个测试类。

  • 可提高复用性
  • 可提高可维护性
  • 面向对象开发的要求

建议:

开闭原则是最基础的原则,前5个原则都是开闭原则的具体形态。

【软件设计】六大设计原则讲解相关推荐

  1. IOS设计模式的六大设计原则之开放-关闭原则(OCP,Open-Close Principle)

    定义 一个软件实体(如类.模块.函数)应当对扩展开放,对修改关闭. 定义解读 在项目开发的时候,都不能指望需求是确定不变化的,大部分情况下,需求是变化的.那么如何应对需求变化的情况?这就是开放-关闭原 ...

  2. 六大设计原则SOLID

    六大设计原则SOLID 一.SOLID 设计模式的六大原则 二.单一职责原则 (Single Responsibility Principle) 1. 单一职责原则定义 2. 问题由来 3. 解决方案 ...

  3. 【设计模式学习笔记】1:认识六大设计原则(OCP,LSV,DIP,ISP,LKP,SRP)

    [1]开闭原则(Open Close Principle) 简述 对扩展是开放的,对修改是关闭的.即软件应当通过扩展来实现变化,而不是通过修改现有的代码. 理解 Java中的继承extends关键字本 ...

  4. [转] 设计模式的六大设计原则

    转载说明: 感谢原作者吕震宇老师的分享. 原文参考链接:https://www.cnblogs.com/zhenyulu/category/6930.html? 本次转载只用于个人学习使用,并不涉及商 ...

  5. 设计模式的分类和六大设计原则

    学习设计模式我是大学研究<java与模式这本书>1024页,很多没有看懂,并且没有总结起来,这次一定要把设计原则和设计模式总结清楚. 设计模式的分类 设计模式分为三大类: 创建型模式,共五 ...

  6. 【0718作业】收集和整理面向对象的六大设计原则

    面向对象的六大设计原则 (1)单一职责原则--SRP (2)开闭原则--OCP (3)里式替换原则--LSP (4)依赖倒置原则--DIP (5)接口隔离原则--ISP (6)迪米特原则--LOD - ...

  7. java设计模式总结之六大设计原则(有图有例子)

    转载:https://www.cnblogs.com/jpfss/p/9765239.html 下面来总结下自己所学习的设计模式,首先我们看下各个模式之间的关系图,下面这张图是网上比较典型的一个类图关 ...

  8. 引用防删——JAVA设计模式总结之六大设计原则

    JAVA设计模式总结之六大设计原则 从今年的七月份开始学习设计模式到9月底,设计模式全部学完了,在学习期间,总共过了两篇:第一篇看完设计模式后,感觉只是脑子里面有印象但无法言语.于是决定在看一篇,到9 ...

  9. JAVA设计模式总结之六大设计原则(一)

    从今年的七月份开始学习设计模式到9月底,设计模式全部学完了,在学习期间,总共过了两篇:第一篇看完设计模式后,感觉只是脑子里面有印象但无法言语.于是决定在看一篇,到9月份第二篇设计模式总于看完了,这一篇 ...

  10. 设计模式之六大设计原则【入门】

    设计模式之六大设计原则 1 开闭原则 Open Closed Principle,OCP 1.1 概念 1.2 软件实体 1.3 开闭原则的作用 2. 单一职责原则 Single responsibi ...

最新文章

  1. 使用Python,OpenCV进行去水印,图像修复
  2. 数据关系模式设计的标准化
  3. UVA11825: Hackers' Crackdown (状压dp)
  4. springboot使用imageio返回图片_SpringBoot 二维码生成(复制即用)
  5. Windows 下开发PHP扩展资源
  6. HTTP 笔记与总结(7)HTTP 缓存(配合 Apache 服务器)
  7. 我们真的需要统一的编程规范?
  8. 第六次作业之图形界面
  9. 【quickhybrid】H5和原生的职责划分
  10. 为什么在C ++中从stdin读取行比Python慢​​得多?
  11. Linux之squirrelmail小松鼠客户端搭建
  12. Python 【第十章】 Django路由
  13. gcc/g++参数详解
  14. tomcat中开启SSL
  15. Mathtype公式编辑器常用快捷键
  16. html 新浪微博分享申请,新浪微博API申请流程详解
  17. 虚拟机vBox xp系统无法联网
  18. 如何制作基于beaglebone的设备通知Texter ?---凯利讯半导体
  19. 【论文精读】Grounded Language-Image Pre-training(GLIP)
  20. win10 屏幕保护程序“在恢复时显示登录屏幕”灰色

热门文章

  1. BugKu CTF(杂项篇MISC)---细心的大象
  2. 氢氧化锂制备系统——吸附(除杂\提锂)+双极膜电渗析
  3. 论天龙八部和程序员的关系
  4. ZBrush: Alpha纹理生成雕花
  5. 【工作经验分享】,大厂面试经验分享
  6. QQ空间日志说说类网站织梦模板(带手机端)
  7. ROS下如何使用moveit驱动UR5机械臂
  8. 睡眠周期检测与吸引力法则
  9. 衢州学院计算机应用技术分数线,2021年衢州学院投档线及各省最低录取分数线统计表...
  10. Editplus激活码2019.5--亲测可用