设计模式七大原则

设计原则是思想上的指导,而设计模式是实现上的手段,是针对某个场景下某些问题的某个解决方案,因此设计模式应该遵 守这些原则,换句话说,设计模式就是这些设计原则的一些具体体现。
设计模式常用的七大原则有:[SOLID CD]

  • S单一职责原则 ——一个类只负责一个功能领域中的相应职责
  • O开闭原则 ——对扩展开放,对修改关闭
  • L里氏代换原则 ——所有引用基类的地方必须能透明地使用其子类的对象
  • D依赖倒转原则 ——依赖于抽象,不能依赖于具体实现
  • I接口隔离原则 ——类之间的依赖关系应该建立在最小的接口上
  • C合成/聚合复用原则 ——尽量使用合成/聚合,而不是通过继承达到复用的目的
  • D迪米特法则 ——一个软件实体应当尽可能少的与其他实体发生相互作用

    1.单一职责原则(Single Responsibility Principle)

对于一个类,只有一个引起该类变化的原因;该类的职责是唯一的,且这个职责是唯一引起其他类变化的原因。

如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。

解决:将不同的职责封装到不同的类或者模块中。 当有新的需求将现有的职责分为颗粒度更小的职责的时候,应该及时对现有代码进行重构。当系统逻辑足够简单,方法足够少,子类够少或后续关联够少时,也可以不必严格遵循你SRP原则,避免过度设计、颗粒化过于严重。

一个类不能做太多的东西。在软件系统中,一个类(一个模块、或者一个方法)承担的职责越多,那么其被复用的可能性就会越低。如类 A 负责两个不同职责:职责 1,职责 2。当职责 1 需求变更而改变 A 时,可能造成职责 2 执行错误,所以需要将类 A 的粒度分解为 A1,A2。

  1. 降低类的复杂度,一个类只负责一项职责。
  2. 提高类的可读性,可维护性
  3. 降低变更引起的风险
  4. 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一职责原则

2.开放-关闭原则OCP(Open-Closed Principle)

所谓开放一关闭原则,指的是,一个软件实体应该对扩展开放,对修改关闭。软件实体的行为是可以扩展的,而且是在不修改已有代码的情况下进 行扩展,也不必改动已有的源代码或者二进制代码。

在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。

开闭原则的意义:
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

如何实现对扩展开放,对修改关闭?
要实现对扩展开放,对修改关闭,即遵循开闭原则,需要对系统进行抽象化设计,抽象可以基于抽象类或者接口。一般来说需要做到几点:

  1. 通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法,也就是扩展必须添加具体实现而不是改变具体的方法。
  2. 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类,这样就能尽量保证抽象层是稳定的。
  3. 一般抽象模块设计完成(例如接口的方法已经敲定),不允许修改接口或者抽象方法的定义。

3.里氏代换原则(Liskov Substitution Principle)

所有引用基类的地方必须能透明地使用其子类的对象,也可以简单理解为任何基类可以出现的地方,子类一定可以出现。

里氏代换原则的意义:只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。

里氏代换原则是对”开-闭”原则的补充。实现”开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。当然,如果反过来,软件单位使用的是一个子类对象的话,那么它不一定能够使用基类对象。

举个很简单的例子说明这个问题:如果一个方法接收Map类型参数,那么它一定可以接收Map的子类参数例如HashMap、LinkedHashMap、ConcurrentHashMap类型的参数;但是返过来,如果另一个方法只接收HashMap类型的参数,那么它一定不能接收所有Map类型的参数,否则它可以接收LinkedHashMap、ConcurrentHashMap类型的参数。

子类为什么可以替换基类的位置?
其实原因很简单,只要存在继承关系,基类的所有非私有属性或者方法,子类都可以通过继承获得(白箱复用),反过来不成立,因为子类很有可能扩充自身的非私有属性或者方法,这个时候不能用基类获取子类新增的这些属性或者方法。

里氏代换原则是实现开闭原则的基础,它告诉我们在设计程序的时候进可能使用基类进行对象的定义和引用,在运行时再决定基类的具体子类型。从另外一个角度来说,里氏替换原则是实现开闭的主要原则之一。开闭原则要求对 扩展开放,扩展的一个实现手段就是使用继承;而里氏替换原则是保证子类型能够正确 替换父类型,只有能正确替换,才能实现扩展,否则扩展了也会出现错误。

4.依赖倒转原则(Dependency Inversion Principle,DIP)

依赖倒转原则(Dependency Inversion Principle,DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。

依赖倒转原则的意义:

依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。
为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。

在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。

依赖倒转原则的注意事项:

  • 高层模块不应该依赖低层模块,高层模块和低层模块都应该依赖于抽象。
  • 抽象不应该依赖于具体,具体应该依赖于抽象。

在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。Spring的IOC是此实现的典范。

从Java角度看待依赖倒转原则的本质就是:面向接口(抽象)编程。

  • 每个具体的类都应该有其接口或者基类,或者两者都具备。
  • 类中的引用对象应该是接口或者基类。
  • 任何具体类都不应该派生出子类。
  • 尽量不要覆写基类中的方法。
  • 结合里氏代换原则使用。

5.接口隔离原则(Interface Segregation Principle)

使用多个专门的接口,而不使用单一的总接口。即客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。

简单来说就是建立单一的接口,不要建立臃肿庞大的接口。也就是接口尽量细化,同时接口中的方法尽量少。

如何看待接口隔离原则和单一职责原则?
单一职责原则注重的是类和接口的职责单一,这里职责是从业务逻辑上划分的,但是在接口隔离原则要求当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。也就是说,我们在设计接口的时候有可能满足单一职责原则但是不满足接口隔离原则。

接口隔离原则的规范:

  • 使用接口隔离原则前首先需要满足单一职责原则。
  • 接口需要高内聚,也就是提高接口、类、模块的处理能力,少对外发布public的方法。
  • 定制服务,就是单独为一个个体提供优良的服务,简单来说就是拆分接口,对特定接口进行定制。
  • 接口设计是有限度的,接口的设计粒度越小,系统越灵活,但是值得注意不能过小,否则变成”字节码编程”。

6.合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)

一般也叫合成复用原则(Composite Reuse Principle, CRP),定义是:尽量使用合成/聚合,而不是通过继承达到复用的目的。

合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向内部持有的这些对象的委派达到复用已有功能的目的,而不是通过继承来获得已有的功能。

  • 聚合(Aggregate)的概念:聚合表示一种弱的”拥有”关系,一般表现为松散的整体和部分的关系,其实,所谓整体和部分也可以是完全不相关的。例如A对象持有B对象,B对象并不是A对象的一部分,也就是B对象的生命周期是B对象自身管理,和A对象不相关。
  • 合成(Composite)的概念:合成表示一种强的”拥有”关系,一般表现为严格的整体和部分的关系,部分和整体的生命周期是一样的。

为什么要用合成/聚合来替代继承达到复用的目的?

  • 继承复用破坏包装,因为继承将基类的实现细节暴露给派生类,基类的内部细节通常对子类来说是可见的,这种复用也称为"白箱复用"。这里有一个明显的问题是:派生类继承自基类,如果基类的实现发生改变,将会影响到所有派生类的实现;如果从基类继承而来的实现是静态的,不可能在运行时发生改变,不够灵活。
  • 由于合成或聚合关系可以将已有的对象,一般叫成员对象,纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为"黑箱复用",相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;
  • 合成/聚合复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。

一般而言,如果两个类之间是“Has-A”的关系,应使用组合或聚合,如果是“Is-A”关系,可使用继承。“Is-A”是严格的分类学意义上的定义,意思是一个类是另一个类的“一种”;而“Has-A”则不同,它表示某一个角色具有某一项责任。

7.迪米特法则(Law of Demeter,LOD)

也叫做最少知识原则(Least Knowledge Principle,LKP),它的定义是:一个软件实体应当尽可能少地与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类(中间类或者跳转类)来转达。

迪米特法则的规则:

  • Only talk to your immediate friends(只与直接的朋友通讯),一个对象的”朋友”包括
    • 当前对象本身(this)。
    • 以参数形式传入到当前对象方法中的对象。
    • 当前对象的成员对象。
    • 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
    • 当前对象所创建的对象。
  • 尽量少发布public的变量和方法,一旦公开的属性和方法越多,修改的时候影响的范围越大。
  • “是自己的就是自己的”,如果一个方法放在本类中,既不产生新的类间依赖,也不造成负面的影响,那么这个方法就应该放在本类中。

迪米特法则的意义:
迪米特法则的核心观念就是类间解耦,也就降低类之间的耦合,只有类处于弱耦合状态,类的复用率才会提高。所谓降低类间耦合,实际上就是尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。但是这样会引发一个问题,有可能产生大量的中间类或者跳转类,导致系统的复杂性提高,可维护性降低。如果一味追求极度解耦,那么最终有可能变成面向字节码编程甚至是面向二进制的0和1编程。