设计模式的六大原则有:
(1)Single Responsibility Principle:单一职责原则
(2)Open Closed Principle:开闭原则
(3)Liskov Substitution Principle:里氏替换原则
(4)Law of Demeter:迪米特法则
(5)Interface Segregation Principle:接口隔离原则
(6)Dependence Inversion Principle:依赖倒置原则
把这六个原则的首字母联合起来(两个 L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。
1.单一职责原则(Single Responsibility Principle)
简称SRP,官方定义为“There should never be more than one reason for a class to change”,译为“一个类应该只有一个发生变化的原因”。
单一职责原则简称 SRP ,顾名思义,就是一个类只负责一个职责。那这个原则有什么用呢,它让类的职责更单一。这样的话,每个类只需要负责自己的那部分,类的复杂度就会降低。如果职责划分得很清楚,那么代码维护起来也更加容易。试想如果所有的功能都放在了一个类中,那么这个类就会变得非常臃肿,而且一旦出现bug,要在所有代码中去寻找;更改某一个地方,可能要改变整个代码的结构,想想都非常可怕。当然一般时候,没有人会去这么写的。
1.1.SRP适用于类、接口、方法
当然,这个原则不仅仅适用于类,对于接口和方法也适用,即一个接口/方法,只负责一件事,这样的话,接口就会变得简单,方法中的代码也会更少,易读,便于维护。
事实上,由于一些其他的因素影响,类的单一职责在项目中是很难保证的。通常,接口和方法的单一职责更容易实现。
1.2.SRP的优点
单一职责原则主要有以下优点:
(1)代码的粒度降低了,类的复杂度降低了。
(2)可读性提高了,每个类的职责都很明确,可读性自然更好。
(3)可维护性提高了,可读性提高了,一旦出现 bug ,自然更容易找到他问题所在。
(4)改动代码所消耗的资源降低了,更改的风险也降低了。
2.开闭原则(Open Closed Principle)
简称OCP,官方定义为“Software entities like classes, modules and functions should be open for extension but closed for modification”,译为“一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭”。
2.1.变化带来的问题
在软件的生命周期内,因为变化,升级和维护等原因需要对软件原有代码进行修改,可能会给旧代码引入错误,也有可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
2.2.用OCP改善因变化带来的问题
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。
开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统,开闭原则只定义了对修改关闭,对扩展开放。其实只要遵循SOLID中的另外5个原则,设计出来的软件就是符合开闭原则的。
2.3.用抽象构建架构,用实现扩展细节
用抽象构建架构,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保证架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了,当然前提是抽象要合理,要对需求的变更有前瞻性和预见性。
3.里氏替换原则(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”,译为“所有引用基类的地方必须能透明地使用其子类的对象”。
3.1.LSP弥补继承的缺陷
LSP的意思是,所有基类在的地方,都可以换成子类,程序还可以正常运行。这个原则是与面向对象语言的继承特性密切相关的。
在学习java类的继承时,我们知道继承有一些优点:
(1)子类拥有父类的所有方法和属性,从而可以减少创建类的工作量。
(2)提高了代码的重用性。
(3)提高了代码的扩展性,子类不但拥有了父类的所有功能,还可以添加自己的功能。
但有优点也同样存在缺点:
(1)继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
(2)降低了代码的灵活性。因为继承时,父类会对子类有一种约束。
(3)增强了耦合性。当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。
如何扬长避短呢?方法是引入里氏替换原则。
3.2.LSP对继承进行了规则上的约束
LSP对继承进行了规则上的约束,这种约束主要体现在四个方面:
(1)子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
(2)子类中可以增加自己特有的方法。
(3)当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比- 父类方法的输入参数更宽松。(即只能重载不能重写)
(4)当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
下面对以上四个含义进行详细的阐述
3.2.1.子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法
在我们做系统设计时,经常会设计接口或抽象类,然后由子类来实现抽象方法,这里使用的其实就是里氏替换原则。若子类不完全对父类的方法进行实例化,那么子类就不能被实例化,那么这个接口或抽象类就毫无存在的意义了。
里氏替换原则规定,子类不能覆写父类已实现的方法。父类中已实现的方法其实是一种已定好的规范和契约,如果我们随意的修改了它,那么可能会带来意想不到的错误。
我们可以给父类的非抽象(已实现)方法加final修饰,这样就在语法层面控制了父类非抽象方法被子类重写而违反里氏替换原则。
有时候父类有多个子类(Son1、Son2),但在这些子类中有一个特例(Son2)。要想满足里氏替换原则,又想满足这个子类的功能时,有的伙伴可能会修改父类(Father)的方法。但是,修改了父类的方法又会对其他的子类造成影响,产生更多的错误。这是怎么办呢?我们可以为这个特例(Son2)创建一个新的父类(Father2),这个新的父类拥有原父类的部分功能(Father2并不继承Father,而是持有Father的一个引用,组合Father,调用Father里的功能),又有不同的功能。这样既满足了里氏替换原则,又满足了这个特例的需求。
3.2.2.子类中可以增加自己特有的方法
这个很容易理解,子类继承了父类,拥有了父类和方法,同时还可以定义自己有,而父类没有的方法。这是在继承父类方法的基础上进行功能的扩展,符合里氏替换原则。
3.2.3.当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
我们应当主意,子类并非重写了父类的方法,而是重载了父类的方法。因为子类和父类的方法的输入参数是不同的。子类方法的参数Map比父类方法的参数HashMap的范围要大,所以当参数输入为HashMap类型时,只会执行父类的方法,不会执行子类的重载方法。这符合里氏替换原则。
在父类方法没有被重写的情况下,子方法被执行了,这样就引起了程序逻辑的混乱。所以子类中方法的前置条件必须与父类中被覆写的方法的前置条件相同或者更宽松。
3.2.4.当子类的方法实现父类的(抽象)方法时,方法的后置条件(即方法的返回值)要比父类更严格
注意:是实现父类的抽象方法,而不是父类的非抽象(已实现)方法,不然就违法了第一条。
若在继承时,子类的方法返回值类型范围比父类的方法返回值类型范围大,在子类重写该方法时编译器会报错。(Java语法)
4.迪米特法则(Law of Demeter)
简称LOD,官方定义为“Talk only to your immediate friends and not to strangers”,译为“只与你的直接朋友交谈,不跟陌生人说话”。
LOD的含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
4.1.LOD的优点
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。
(1)降低了类之间的耦合度,提高了模块的相对独立性。
(2)由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
4.2.掌握使用迪米特法则的平衡
过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
4.3.迪米特法则的实现方法
从迪米特法则的定义和特点可知,它强调以下两点:
(1)从依赖者的角度来说,只依赖应该依赖的对象。
(2)从被依赖者的角度说,只暴露应该暴露的方法。
4.4.迪米特法则案例
一个中介,客户只要找中介要满足的楼盘 ,而不必跟每个楼盘发生联系。
微服务中的网关,前端都请求到网关,而不是直接请求具体的微服务。
5.接口隔离原则(Interface Segregation Principle)
简称ISP,官方有两个定义: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。翻译过来就是:客户端不应该依赖它不需要的接口;类间的依赖关系应该建立在最小的接口上。
以上两个定义的含义是:要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
5.1.ISP和SRP的区别
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
5.2.ISP的优点
接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。
(1)将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
(2)接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
(3)如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
(4)使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
(5)能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
5.3.ISP的实现方法
在具体应用接口隔离原则时,应该根据以下几个规则来衡量。
(1)根据接口隔离原则拆分接口时,首先必须满足单一职责原则。
(2)接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
(3)为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
(4)了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
(5)提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
6.依赖倒置原则(Dependence Inversion Principle)
简称DIP,官方定义有两条: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。译为:上层模块不应该依赖底层模块,它们都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
首先,这个原则听起来很像是“针对接口编程,不针对现实编程”,不是吗?的确很相似,然而这里更强调“抽象”。
依赖倒置原则,究竟倒置在哪里?
在依赖倒置原则中的倒置指的是和一般OO设计的思考方式完全相反。