设计模式六大原则(SOLID)
Single Responsibility Principle:单一职责原则
Open Closed Principle:开闭原则
Liskov Substitution Principle:里氏替换原则
Law of Demeter:迪米特法则
Interface Segregation Principle:接口隔离原则
Dependence Inversion Principle:依赖倒置原则
把这六个原则的首字母联合起来(两个 L 算做一个)恰好是 SOLID (solid,稳定的)这个单词,其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。
1. 开闭原则 (Open Closed Principle, OCP)
开闭原则是面向对象设计的终极目标。其他几条,则可以看做是开闭原则的实现方法。 设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。开闭原则是面向对象设计中最基础最重要的设计原则。
Software entities like classes, modules and functions should be open for extension but closed for modification.
一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭
-
一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭.
-
变化带来的问题
-
修改原有代码可能引入错误
-
必要时会不得不重构
-
经过修改后就需要重新测试
-
时间和财力成本增加
-
-
如何实现开闭原则
-
只要遵循SOLID中的另外5个原则,设计出来的软件就是符合开闭原则的。
-
用抽象构建架构,用实现扩展细节
-
用抽象构建架构,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保证架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了,当然前提是抽象要合理,要对需求的变更有前瞻性和预见性。
-
-
2. 里氏替换原则 (Liskov Substitution Principle, LSP)
里氏替换原则的意思是,所有基类所在的地方,都可以换成子类,程序还可以正常运行。这个原则是与面向对象语言的继承特性密切相关的。
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。
Functions that use use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基类的地方必须能透明地使用其子类的对象。
-
面向对象语言一般具有的继承特性:
-
优点
-
提高代码重用性:子类拥有父类所有的方法和属性
-
提高代码的扩展性:子类不但拥有父类全部方法,还可以添加自己的功能
-
-
缺点
-
增强了耦合性:当需要修改父类的方法时,必须考虑对子类产生的影响
-
父类对子类的透明性:只要继承,父类的方法和属性就都被子类拥有
-
子类被父类约束:父类的某些属性或方法约束了子类的某些属性和方法
-
-
-
里氏替换原则对继承进行了规则上的约束(四个方面)
-
子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法;
-
子类中可以增加自己特有的方法;
-
只能重载不能重写:当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
public class Father { public void fun(HashMap map){ System.out.println("父类被执行..."); } } public class Son extends Father { public void fun(Map map){ System.out.println("子类被执行..."); } } public class Client { public static void main(String[] args) { System.out.print("父类的运行结果:"); Father father=new Father(); HashMap map=new HashMap(); father.fun(map); //父类存在的地方,可以用子类替代 //子类B替代父类A System.out.print("子类替代父类后的运行结果:"); Son sun=new Son(); son.fun(map); } }
输出:
父类的运行结果:父类被执行... 子类替代父类后的运行结果:父类被执行...
java中HashMap是Map接口的实现,所以参数Map比父类HashMap的范围要大,所以当参数输入为HashMap类型只会执行父类的方法,不会执行子类的重载方法。这符合里氏替换原则。
但如果子类的参数范围小于父类,则会产生相反的结果:
public class Father { public void fun(Map map){ System.out.println("父类被执行..."); } } public class Son extends Father { public void fun(HashMap map){ System.out.println("子类被执行..."); } } public class Client { public static void main(String[] args) { System.out.print("父类的运行结果:"); Father father=new Father(); HashMap map=new HashMap(); father.fun(map); //父类存在的地方,可以用子类替代 //子类B替代父类A System.out.print("子类替代父类后的运行结果:"); Son son=new Son(); son.fun(map); } }
输出:
父类的运行结果:父类被执行... 子类替代父类后的运行结果:子类被执行...
在父类方法没有被重写的情况下,子方法被执行了,这样就引起了程序逻辑的混乱。所以子类中方法的前置条件必须与父类中被覆写的方法的前置条件相同或者更宽松。
-
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
public abstract class Father { public abstract Map fun(); } public class Son extends Father { @Override public HashMap fun() { System.out.println("子类被执行..."); return null; } } public class Client { public static void main(String[] args) { Father father=new Son(); father.fun(); } }
输出:
子类被执行...
注意:是实现父类的抽象方法(接口),而不是父类的非抽象(已实现)方法,不然就违背了第一条。
若在继承时,子类的方法返回值类型范围比父类的方法返回值类型范围大,在子类重写该方法时编译器会报错。(Java语法)
-
3. 依赖倒置原则 (Dependency Inversion Principle,OIP)
依耐倒置原则强调的是具体依赖于抽象,具体来说就是细节依赖抽象,上层模块不应该依赖底层模块,它们都应该依赖于抽象。
1、High level modules should not depend upon low level modules. Both should depend upon abstractions. 2、Abstractions should not depend upon details. Details should depend upon abstractions.
1、上层模块不应该依赖底层模块,它们都应该依赖于抽象。 2、抽象不应该依赖于细节,细节应该依赖于抽象。
举个例子:你想开一家披萨店,你首先想到的结构是这样的:
这种下层依赖上层,具体依赖具体的结构就是不希望出现的。你的思想也是这样“先把披萨店开起来,再考虑卖什么披萨”。
现在需要“倒置”你的思想,先考虑我会卖一些披萨,我需要抽象一个披萨接口,具体的披萨类型是这个披萨接口的实现,而我的披萨店应该依耐的也是这个披萨接口,接着设计这个披萨店。依赖倒置后的结构就应该是:
这个例子就解释了 上层模块不应该依赖底层模块 和 抽象不依赖于具体。
4. 单一职责原则(Single Responsibility Principle)
也称为合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
There should never be more than one reason for a class to change.
一个类应该只有一个发生变化的原因!
-
不只是一个类,一个模块应该只有一个发生变化的原因。一个模块可以是一个类,一个接口,一个方法(函数)。
-
好处:
-
降低了每个模块的复杂度,每个模块职责划分得很清楚,便于代码维护,避免“牵一发而动全身”
-
提高了代码可读性,在发生Bug的时候方便快速定位问题所在
-
代码改动的成本降低了,修改的地方所牵连的越少,更改风险越少
-
5. 接口隔离原则(Interface Segregation Principle,ISP)
接口隔离原则的意思是:应该为各个类建立所需要的专用接口,而不要试图建立一个很庞大的接口供所有依赖它的类调用。这样可以降低软件架构的耦合性:约束接口、降低类对接口的依赖性。
1、Clients should not be forced to depend upon interfaces that they don`t use. 2、The dependency of one class to another one should depend on the smallest possible.
1、客户端不应该依赖它不需要的接口。 2、类间的依赖关系应该建立在最小的接口上。
-
接口隔离原则的优点:
-
将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
-
接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
-
如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
-
使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
-
能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
-
-
接口隔离的一般实现方法
-
根据接口隔离原则拆分接口时,首先必须满足单一职责原则
-
接口尽量小——一个接口只服务于一个子模块或者业务逻辑
-
为依赖接口的类定制服务——只提供调用者需要的方法,屏蔽不需要的方法
-
接口要深入贴合业务逻辑——每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同。要深入了解业务逻辑,拒绝“同而不合”
-
提高内聚,减少对外交互——使接口用最少的方法完成最多的事情
-
-
接口隔离和单一职责的异同
-
相同点:接口隔离和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想
-
不同点:
-
单一职责原则注重职责,接口隔离原则注重的是对接口依赖的隔离
-
单一职责原则主要是约束类,它针对的是程序中的实现和细节,而接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
-
-
6. 最小知识原则(迪米特法则)(Principle of Least Knowledge,PLK)
如果两个软件实体无须直接通信,那么就不应该发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类与类之间的耦合度,提高模块的相互独立性。
Talk only to your immediate friends and not to strangers
只与你的直接朋友交谈,不跟“陌生人”说话
-
迪米特法则的优点
-
降低了类之间的耦合度,提高了模块的相互独立性
-
提高了类的复用率和系统的扩展性
-
-
掌握使用迪米特法则的平衡
-
过度使用迪米特法则会导致系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。
-
所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
-
-
迪米特法则的实现方法
-
从依赖者的角度来说,只依赖应该依赖的对象。
-
从被依赖者的角度说,只暴露应该暴露的方法。
-
-
迪米特法则案例
-
买楼:客户只需要找中介咨询满足自己需求的楼盘,而不必跟每个楼盘发生联系。
-
微服务中的网关:前端都请求到网关,而不是直接请求具体的微服务。
-
小结
-
单一职责原则告诉我们实现类要职责单一;
-
里氏替换原则告诉我们不要破坏继承关系;
-
依赖倒置原则告诉我们要面向接口编程;
-
接口隔离原则告诉我们在设计接口的时候要精简单一;
-
迪米特法则告诉我们要降低耦合;
-
开闭原则告诉我们要对扩展开发,对修改关闭;
References
《大话设计模式》(程杰)