面向对象设计原则(二):里氏替换原则与合成复用原则

一、里氏替换原则(LSP)

里氏替换原则(Liskov Substitution Principle,LSP)由麻省理工学院计算机科学实验室的里斯科夫(Liskov)女士在 1987 年的“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstraction and Hierarchy)里提出来的,她提出:继承必须确保超类所拥有的性质在子类中仍然成立(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)。里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。

定义

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

定义2:所有引用基类的地方必须能透明地使用其子类的对象。

里式替换原则的引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能。

重构违反LSP的设计

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

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

  • 从B到A的继承关系改为关联关系。

对于矩形和正方形例子,可以构造一个抽象的四边形类,把矩形和正方形共同的行为放到这个四边形类里面,让矩形和正方形都是它的派生类,问题就OK了。对于矩形和正方形,取width 和height 是它们共同的行为,但是给width 和height 赋值,两者行为不同,因此,这个抽象的四边形的类只有取值方法,没有赋值方法。

运动员和自行车例子,每个运动员都有一辆自行车,如果按照下面设计,很显然违反了LSP原则。

class Bike {
public:
       void Move( );
       void Stop( );
       void Repair( );
protected:
       int    ChangeColor(int );
private:
       int    mColor;
};


class Player : private Bike
{
public:
      void  StartRace( );
      void  EndRace( ); 
protected:
       int    CurStrength ( ); 
private:
        int   mMaxStrength;
        int   mAge;
};

对于运动员和自行车例子,可以采用关联关系来重构:

class Player 
{
public:
      void  StartRace( );
      void  EndRace( ); 
protected:
       int    CurStrength ( ); 
private:
        int   mMaxStrength;
        int   mAge;
Bike * abike;
};

在进行设计的时候,我们尽量从抽象类继承,而不是从具体类继承。

如果从继承等级树来看,所有叶子节点应当是具体类,而所有的树枝节点应当是抽象类或者接口。当然这只是一个一般性的指导原则,使用的时候还要具体情况具体分析。

在很多情况下,在设计初期我们类之间的关系不是很明确,LSP则给了我们一个判断和设计类之间关系的基准:需不需要继承,以及怎样设计继承关系。

二、合成复用原则

以下内容参考:Java设计模式——合成/聚合复用原则 https://blog.csdn.net/u010832572/article/details/45007933

什么是合成/聚合复用原则?

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

简而言之,对于合成/聚合复用原则的定义就是:要尽量使用合成和聚合,尽量不要使用继承。

为什么使用合成/聚合复用,而不使用继承复用?

在面向对象的设计里,有两种基本的方法可以在不同的环境中复用已有的设计和实现,即通过合成/聚合复用和通过继承复用。两者的特点和区别,优点和缺点如下。

1、合成/聚合复用

由于合成或聚合可以将已有对象纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能。这样做的好处有:

(1)新对象存取成分对象的唯一方法是通过成分对象的接口。

(2)这种复用是黑箱复用,因为成分对象的内部细节是新对象看不见的。

(3)这种复用支持包装。

(4)这种复用所需的依赖较少。

(5)每一个新的类可以将焦点集中到一个任务上。

(6)这种复用可以再运行时间内动态进行,新对象可以动态地引用与成分对象类型相同的对象。

一般而言,如果一个角色得到了更多的责任,那么可以使用合成/聚合关系将新的责任委派到合适的对象。当然,这种复用也有缺点。最主要的缺点就是通过这种复用建造的系统会有较多的对象需要管理。

2、继承复用

继承复用通过扩展一个已有对象的实现来得到新的功能,基类明显的捕获共同的属性和方法,而子类通过增加新的属性和方法来扩展超类的实现。继承是类型的复用。

继承复用的优点。

(1)新的实现较为容易,因为超类的大部分功能可以通过继承关系自动进入子类。

(2)修改或扩展继承而来的实现较为容易。

继承复用的缺点。

(1)继承复用破坏包装,因为继承将超类的实现细节暴露给了子类。因为超类的内部细节常常对子类是透明的,因此这种复用是透明的复用,又叫“白箱”复用。

(2)如果超类的实现改变了,那么子类的实现也不得不发生改变。因此,当一个基类发生了改变时,这种改变会传导到一级又一级的子类,使得设计师不得不相应的改变这些子类,以适应超类的变化。

(3)从超类继承而来的实现是静态的,不可能在运行时间内发生变化,因此没有足够的灵活性。

由于继承复用有以上的缺点,所有尽量使用合成/聚合而不是继承来达到对实现的复用,是非常重要的设计原则。

从代码重构的角度理解

一般来说,对于违反里氏代换原则的设计进行重构时,可以采取两种方法:一是加入一个抽象超类;二是将继承关系改写为合成/聚合关系。要正确的使用继承关系,必须透彻的理解里氏代换原则和Coad条件。

何时使用合成/聚合、继承的两种判断方法:

A.Coad条件

区分“Has-A”和“Is -A”:“Is-A”代表一个类是另外一个类的一种,可以使用继承关系,而“Has-A”代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。

下面类图中描述的例子。“人”被继承到“学生”、“经理”和“雇员”等子类。而实际上,学生”、“经理”和“雇员”分别描述一种角色,而“人”可以同时有几种不同的角色。比如,一个人既然是“经理”,就必然是“雇员”;而“人”可能同时还参加MBA课程,从而也是一个“学生”。使用继承来实现角色,则只能使每一个“人”具有Is-A角色,而且继承是静态的,这会使得一个“人”在成为“雇员”身份后,就永远为“雇员”,不能成为“学生”和“经理”,而这显然是不合理的。

面向对象设计原则(二):里氏替换原则与合成复用原则_第1张图片

这一错误的设计源自于把“角色”的等级结构和“人”的等级结构混淆起来,把“Has-A”角色误解为“Is -A”角色。因此要纠正这种错误,关键是区分“人”与“角色”的区别。下图所示的的设计就正确的做到了这一点。

面向对象设计原则(二):里氏替换原则与合成复用原则_第2张图片

从上图可以看出,每一个“人”都可以有一个以上的“角色”,所有一个“人”可以同时是“雇员”,又是“经理”,甚至同时又是“学生”。而且由于“人”与“角色”的耦合是通过合成的,因此,角色可以有动态的变化。一个“人”可以开始是“雇员”,然后晋升为“经理”,然后又由于他参加了MBA课程,又称为了“学生“。

当一个类是另一个类的角色时,不应当使用继承描述这种关系。

B.里氏代换原则

里氏代换原则是继承复用的基石。如果在任何可以使用B类型的地方都可以使用S类型,那么S类型才可以称为B类型的子类型(SubType),而B类型才能称为S类型的基类型(BaseType)。

换言之,只有当每一个S在任何情况下都是一种B的时候,才可以将S设计成B的子类。如果两个类的关系是“Has-A”关系而不是“Is -A”,这两个类一定违反里氏代换原则。

只有两个类满足里氏代换原则,才有可能是“Is -A”关系。

参考:

Java设计模式——合成/聚合复用原则 https://blog.csdn.net/u010832572/article/details/45007933
面向对象开发方法,Coad方法、Booch方法和OMT方法及UML https://blog.csdn.net/qq_34781336/article/details/84585427
面向对象设计的七大设计原则详解 https://blog.csdn.net/qq_34760445/article/details/82931002

合成复用原则——面向对象设计原则 http://c.biancheng.net/view/1333.html
里氏替换原则——面向对象设计原则 http://c.biancheng.net/view/1324.html

设计模式六大原则(2):里氏替换原则 https://blog.csdn.net/zhengzhb/article/details/7281833

你可能感兴趣的:(设计模式,设计模式)