面向对象的设计原则

简述

面向对象的设计原则有七个,包括:开闭原则、里氏代换原则、迪米特原则(最少知道原则)、单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则。
一般地,可以把这七个原则分成了以下两个部分:

  • 设计目标:开闭原则、里氏代换原则、迪米特原则
  • 设计方法:单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则

一、开闭原则(The Open-Closed Principle ,OCP)

概念理解

  • 扩展开放:某模块的功能是可扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。
  • 修改关闭:某模块被其他模块调用,如果该模块的源代码不允许修改,则该模块修改关闭的。软件系统的功能上的稳定性,持续性要求模块是修改关闭的。

系统设计需要遵循开闭原则的原因

  • 稳定性。开闭原则要求扩展功能不修改原来的代码,这可以让软件系统在变化中保持稳定。
  • 扩展性。开闭原则要求对扩展开放,通过扩展提供新的或改变原有的功能,让软件系统具有灵活的可扩展性。
    遵循开闭原则的系统设计,可以让软件系统可复用,并且易于维护。

示例

二、里氏替换原则(Liskov Substitution Principle ,LSP)

概念理解

(1)应该满足下面两个条件

  • 不应该在代码中出现if/else之类对派生类类型进行判断的条件。
  • 派生类应当可以替换基类并出现在基类能够出现的任何地方,或者说如果我们把代码中使用基类的地方用它的派生类所代替,代码还能正常工作。

(2)里氏替换原则(LSP)是使代码符合开闭原则的一个重要保证。

(3)同时LSP体现了:

  • 类的继承原则:如果一个派生类的对象可能会在基类出现的地方出现运行错误,则该派生类不应该从该基类继承,或者说,应该重新设计它们之间的关系。

  • 动作正确性保证:从另一个侧面上保证了符合LSP设计原则的类的扩展不会给已有的系统引入新的错误。
    示例:

里式替换原则的引申意义

子类可以扩展父类的功能,但不能改变父类原有的功能。具体来说:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的方法时(重载/重写或实现抽象方法)的后置条件(即方法的输出/返回值)要比父类更严格或相等。

里式替换原则的优点

  • 约束继承泛滥,是开闭原则的一种体现。
  • 加强程序的健壮性,同时变更时也可以做到非常好地提高程序的维护性、扩展性。降低需求变更时引入的风险。

如何重构违反LSP的设计

如果两个具体的类A,B之间的关系违反了LSP 的设计,(假设是从B到A的继承关系),那么根据具体的情况可以在下面的两种重构方案中选择一种:

  • 创建一个新的抽象类C,作为两个具体类的基类,将A,B的共同行为移动到C中来解决问题。
  • 从B到A的继承关系改为关联关系。

三、迪米特原则(最少知道原则)(Law of Demeter ,LoD)

概念理解

  • 一个软件实体应当尽可能少地与其他实体发生相互作用。
  • 每一个软件单位对其他的单位的了解都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

优缺点

  • 迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。

  • 迪米特原则不希望类直接建立直接的接触。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特原则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系,这在一定程度上增加了系统的复杂度。

四、单一职责原则

概念理解

只能让一个类/接口/方法有且仅有一个职责。

为什么要单一职责

如果一个类具有一个以上的职责,那么就会有多个不同的原因引起该类变化,而这种变化将影响到该类不同职责的使用者(不同用户):

  • 一方面,如果一个职责使用了外部类库,则使用另外一个职责的用户却也不得不包含这个未被使用的外部类库。
  • 另一方面,某个用户由于某个原因需要修改其中一个职责,另外一个职责的用户也将受到影响,他将不得不重新编译和配置。
    这违反了设计的开闭原则,也不是我们所期望的。

单一职责定义

所谓一个类的一个职责是指引起该类变化的一个原因。
如果你能想到一个类存在多个使其改变的原因,那么这个类就存在多个职责。

使用单一职责原则的理由

单一职责原则从职责(改变理由)的侧面上为我们对类(接口)的抽象的颗粒度建立了判断基准:在为系统设计类(接口)的时候应该保证它们的单一职责性。
降低了类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险

五、接口分隔原则(Interface Segregation Principle ,ISP)

概念理解

  • 接口的设计原则:接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能专一的接口。
  • 接口的依赖(继承)原则:如果一个接口a继承另一个接口b,则接口a相当于继承了接口b的方法,那么继承了接口b后的接口a也应该遵循上述原则:不应该包含用户不使用的方法。 反之,则说明接口a被b给污染了,应该重新设计它们的关系。

接口分隔原则的优点和适度原则

接口分隔原则从对接口的使用上为我们对接口抽象的颗粒度建立了判断基准:在为系统设计接口的时候,使用多个专门的接口代替单一的胖接口。

  • 符合高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性。
  • 注意适度原则,接口分隔要适度,避免产生大量的细小接口。

单一职责原则和接口分隔原则的区别

  • 单一职责强调的是接口、类、方法的职责是单一的,强调职责,方法可以多,针对程序中实现的细节;
  • 接口分隔原则主要是约束接口,针对抽象、整体框架。

六、依赖倒置原则(Dependency Inversion Principle ,DIP)

概念理解

  • 依赖:在程序设计中,如果一个模块a使用/调用了另一个模块b,我们称模块a依赖模块b。
  • 高层模块与低层模块:往往在一个应用程序中,我们有一些低层次的类,这些类实现了一些基本的或初级的操作,我们称之为低层模块;另外有一些高层次的类,这些类封装了某些复杂的逻辑,并且依赖于低层次的类,这些类我们称之为高层模块。
  • 依赖倒置(Dependency Inversion):
    面向对象程序设计相对于面向过程(结构化)程序设计而言,依赖关系被倒置了。因为传统的结构化程序设计中,高层模块总是依赖于低层模块。

问题的提出:

Robert C. Martin氏在原文中给出了“Bad Design”的定义:

  • 系统很难改变,因为每个改变都会影响其他很多部分。
  • 当你对某地方做一修改,系统的看似无关的其他部分都不工作了。
  • 系统很难被另外一个应用重用,因为很难将要重用的部分从系统中分离开来。
  • 导致“Bad Design”的很大原因是“高层模块”过分依赖“低层模块”。

DIP给出了一个解决方案:在高层模块与低层模块之间,引入一个抽象接口层。


依赖倒置原则的优点

可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险。

七、组合/聚合复用原则(Composite/Aggregate Reuse Principle ,CARP)

概念理解

即在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。就是说要尽量的使用合成和聚合,而不是继承关系达到复用的目的。

组合和聚合都是关联的特殊种类。

聚合表示整体和部分的关系,表示“拥有”。组合则是一种更强的“拥有”,部分和整体的生命周期一样。

组合的新的对象完全支配其组成部分,包括它们的创建和湮灭等。一个组合关系的成分对象是不能与另一个组合关系共享的。

在面向对象设计中,有两种基本的办法可以实现复用:第一种是通过组合/聚合,第二种就是通过继承。

何时使用继承关系:

1)派生类是基类的一个特殊种类,而不是基类的一个角色,也就是区分"Has-A"和"Is-A"。只有"Is-A"关系才符合继承关系,"Has-A"关系应当用聚合来描述。
2)永远不会出现需要将派生类换成另外一个类的派生类的情况。如果不能肯定将来是否会变成另外一个派生类的话,就不要使用继承。
3)派生类具有扩展基类的责任,而不是具有置换掉(override)或注销掉(Nullify)基类的责任。如果一个派生类需要大量的置换掉基类的行为,那么这个类就不应该是这个基类的派生类。
4)只有在分类学角度上有意义时,才可以使用继承。

通过组合/聚合复用的优缺点

(1)优点:

  • 新对象存取子对象的唯一方法是通过子对象的接口。
  • 这种复用是黑箱复用,因为子对象的内部细节是新对象所看不见的。
  • 这种复用更好地支持封装性。
  • 这种复用实现上的相互依赖性比较小。
  • 每一个新的类可以将焦点集中在一个任务上。
  • 这种复用可以在运行时间内动态进行,新对象可以动态的引用与子对象类型相同的对象。
  • 作为复用手段可以应用到几乎任何环境中去。
    (2)缺点:
  • 就是系统中会有较多的对象需要管理。

通过继承来进行复用的优缺点

(1)优点:

  • 新的实现较为容易,因为基类的大部分功能可以通过继承的关系自动进入派生类。
  • 修改和扩展继承而来的实现较为容易。
    (2)缺点:
  • 继承复用破坏封装性,因为继承将基类的实现细节暴露给派生类。由于基类的内部细节常常是对于派生类透明的,所以这种复用是透明的复用,又称“白箱”复用。
  • 如果基类发生改变,那么派生类的实现也不得不发生改变。
  • 从基类继承而来的实现是静态的,不可能在运行时间内发生改变,没有足够的灵活性。

欢迎大家关注我的公众号


半亩房顶

你可能感兴趣的:(面向对象的设计原则)