设计模式系列文章导航

  1. 设计模式概述
  2. 面向对象设计原则 📍当前位置
  3. 设计模式 - 创建型模式
  4. 设计模式 - 结构型模式
  5. 设计模式 - 行为型模式

概述

  • 软件的可维护性[1]可维护性参考资料可复用性[2]可复用性参考资料是两个非常重要的用于衡量软件质量的属性。

    原则名称 英文名称 定义 使用频率
    单一职责原则 Single Responsibility Principle, SRP 一个对象应该只包含单一的职责,并且该职责被完整的封装在一个类中 ★★★★☆
    开闭原则 Open-Closed Principle, OCP 软件实体应当对扩展开放,对修改关闭 ★★★★★
    里氏代换原则 Liskov Substitution Principle, LSP 所有引用基类的地方必须能透明地使用其子类的对象 ★★★★★
    依赖倒转原则 Dependence Inversion Principle, DIP 高层模块不应该以来低层模块,它们都应该依赖抽象;抽象不应该依赖于细节,细节应该依赖于抽象 ★★★★★
    接口隔离原则 Interface Segregation Principle, ISP 客户端不应该依赖那些它不需要的接口 ★★☆☆☆
    合成复用原则 Composite Reuse Principle, CRP 有线使用对象组合,而不是通过继承来打到复用的目的 ★★★★☆
    迪米特法则 Law of Demeter, LoD 每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位 ★★★☆☆

单一职责原则

  • 定义:一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。

  • 另一种定义方式:就一个类而言,应该仅有一个引起它变化的原因。

  • 在软件系统中,一个类承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,相当于将这些职责耦合在一起,当其中一个职责变化时可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将他们封装在同一类中。

    CustomerDataChart类的方法中,getConnection()方法用于连接数据库,findCustomers()用于查询所有的客户信息,createChart()用于创建图表,displayChart()用于显示图表。

    image-20231107110617712

    CustomerDataChart类承担了太多的职责,既包含与数据库相关的方法,又包含与图表生成和显示相关的方法。如果在其他类中也需要连接数据库或者使用findCustomers()方法查询客户信息,则难以实现代码的重用。无论是修改数据库连接方式还是修改图表显示方式都需要修改该类,它拥有不止一个引起它变化的原因,违背了单一职责原则。因此需要对该类进行拆分,使其满足单一职责原则,CustomerDataChart类可拆分为以下3个类。

    1. DBUtil:负责连接数据库,包含数据库连接方法getConnection()
    2. CustomerDAO:负责操作数据库中的Customer表,包含对Customer表的增、删、改、查等方法,例如findCustomers()
    3. CustomerDataChart:负责图表的生成和显示,包含createChart()displayChart()方法

    重构后的结构图:

    image-20231107110956731

开闭原则

  • 开闭原则是面向对象的可复用设计的第一块基石,定义:软件实体应当对扩展开放,对修改关闭,即软件实体应尽量在不修改原有代码的情况下进行扩展
  • 任何软件都需要面临一个很重要的问题,即需求会随着时间的推移而发生变化。当软件系统需要面对新的需求时应尽量保证系统的设计框架是稳定的。如果一个软件设计符合开闭原则,那么可以非常方便地对系统进行扩展,而且在扩展时无须修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。随着软件规模越来越大,寿命越来越长,维护成本也越来越高,设计满足开闭原则的软件也变得越来越重要。为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键,定义系统的抽象层,再通过具体的类进行扩展。

里氏替换原则

如果对每一个类型为S的对象o1都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换o2时程序P的行为没有变化,那么类型S是类型T的子类型。

  • 通俗来说,里氏替换原则即所有引用基类的地方必须能透明地使用其子类的对象,表明在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反之则不成立。比如:如果我喜欢所有动物,那么我一定喜欢狗,因为狗是动物的子类;但如果我喜欢狗,并不能断定我喜欢所有的动物。
  • 里氏替换原则是实现开闭原则的重要方法之一,由于在使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时在确定其子类的类型,用子类对象来替换父类对象。
  • 在运用里氏替换原则时应该将父类设计为抽象类或者接口,让子类继承父类或实现父类接口,并实现在父类中声明的方法,在运行时子类实例替换父类实例,可以很方便地扩展系统的功能,无需修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。

依赖倒转原则

  • 依赖倒转原则是面向对象设计的主要实现机制之一,它是系统抽象化的具体实现。

  • 依赖倒转原则的定义:高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。简单地说,依赖倒转原则要求针对接口编程,不要针对实现编程

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

  • 引入抽象层后,系统具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无需修改原有系统的源代码。

  • 实现依赖倒转原则需要针对抽象层编程,而将具体类通过依赖注入的方式注入到其他对象中。依赖注入是指当一个对象要与其他对象发生依赖关系时采用抽象的形式来注入所依赖的对象。

    常见的注入方式有3种:

    1. 构造注入:通过构造函数来传入具体类的对象
    2. 设值注入(Setter注入):通过Setter方法来传入具体类的对象
    3. 接口注入:通过在接口中声明的业务方法来传入具体类的对象

接口隔离原则

  • 接口隔离原则定义:客户端不应该依赖那些它不需要的接口。当一个接口太大时需要将它分割成一些更细小的接口,使用该接口的客户端仅需要知道与之相关的方法即可。
  • “接口”有两种含义:
    • 如果理解为一个类型所提供的所有方法特征的集合的时候,可以将接口理解成角色,一个接口只能代表一个角色,每个角色都有它特地的一个接口,此时这个原则可以叫“角色隔离原则”。
    • 如果把接口理解成狭义的特定语言的接口,那么接口隔离原则表达的意思是指接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。

合成复用原则

  • 合成复用原则又称组合/聚合复用原则,定义:优先使用对象组合,而不是通过继承来达到复用的目的,即在一个新的对象里通过关联关系(包裹组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分,新对象通过委派调用已有对象的方法达到复用功能的目的。
  • 通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的某些内部细节对于子类来说是可见的,因此这种复用又称为“白箱”复用,如果基类发生改变,那么子类的实现也不得不发生改变。
  • 组合或聚合关系可以将已有的对象(亦可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使成员对象的内部实现对于新对象不可见,所以这种复用又称为“黑箱”复用。

迪米特法则

  • 迪米特法则定义:每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。其要求一个软件实体应当尽可能少地与其他实体发生相互作用。如果一个系统符合迪米特法则,那么当其中的某一个模块发生修改时就会尽量少地影响其他模块,扩展会相对容易。

  • 应用迪米特法则可以降低系统的耦合度,使类与类之间保持松散的耦合关系。

  • 迪米特法则还有几种定义形式,例如:不要和“陌生人”说话,只与你的直接朋友通信,“朋友”包括如下几类:

    1. 当前对象本身
    2. 以参数形式传入到当前对象方法中的对象
    3. 当前对象的成员对象
    4. 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
    5. 当前对象所创建的对象

    这样做可以降低系统的耦合度,一个对象的改变不会给太多其他对象带来影响。

  • 迪米特法则还要求在设计系统时应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中一个对象需要调用另一个对象的方法,可以通过“第三者”转发这个调用。