[OOD]违反里氏替换原则的解决方案

关于OOD中的里氏替换原则,大家耳熟能祥了,不再展开,可以参考设计模式的六大设计原则之里氏替换原则。这里尝试讨论常常违反的两种形式和解决方案。

违反里氏替换原则的根源是对子类及父类关系不明确。我们在设计继承关系常常受一些主观认识的左右,比如Robert C. Martin提到的线段与线的关系,以及被大家说到烂的正方形与矩形。从以前的经验我们认为它们符合继承关系,比如线段是线的较短形式,正方形是矩形的一个特例。但事实上它们并不能完全的包容和替代。

以集合的形式表示,左图是里氏替换的目标,子类可以完全包容了父类的特性集合。右图则是说两者存在不兼容的特性集合:

对应的解决方案就是进一步抽象,将它们之前的关系从语言的角度重新定义,也许果真是is-a, 也许是has-a,也许它们只是兄弟。

基本的思路如下:

1. 找到更高层次的抽象

以Robert C. Martin举的线与线段为例, 初始实现Line是LineSegment的基类:

// Line代表经过两点(P1,P2)的一条的直线
class Line{
 public:
   double GetSlope() const;
   Point GetP1() const;
   Point GetP2() const;
   virtual bool IsOn(const Point&) const;

 private:
  Point itsP1;
  Point itsP2;
}

// LineSegment则是由两点(P1,P2)连接的线段。
class LineSegment : public Line {
 public:
  virtual bool IsOn(const Point&) const;
}

其中IsOn函数用于计算某个点在不在直线或线段上。对于直线而言,一个点在不在其上仅仅取决于这个点相对于直线的两个点的关系。而对于线段而言,还是它是否在线程起止边界内。两者对于这个接口函数的判断条件并不相同,所以LineSegment无法直接代替父类,违反了里氏替换原则。

解决方案是将这个不一致的接口排除掉,剩下的公共接口做为直线和线段的基类,即定义一个LinearObject做为Line及LineSegment的基类:

class Line{
 public:
   double GetSlope() const;
   Point GetP1() const;
   Point GetP2() const;
   // 纯虚函数的意义在于,确保使用基类的客户代码不会使用这个接口函数
   virtual bool IsOn(const Point&) const = 0;

 private:
  Point itsP1;
  Point itsP2;
}

2. 改为has-a关系

另一种解决方案,是针对继承关系太过牵强的情况,比如所谓的is-implemented-in-terms-of (由谁实现)的情况,不如转化为组合模式,如下面的关系:

Scott Meyers在Effective C++ 3e, Item 38提到一个案例。比如准备基于std::list实现一个Set。初步想法是期望保持与list相同的接口,于是定义为:

template<typename T>
class Set : public std::List<T> {...}

但Set与List在行为有一个巨大的差异是Set不允许重复的元素,所以也违反了里氏替换原则。

解决方案就是,使用std::list实现,就是一个has-a关系,可以定义为:

template<typename T>
class Set {
 public:
  void insert(const T& item);
  void remove(const T& item);
  ...

 private:
  std::list<T> rep;
}

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-11-08 01:11:13

[OOD]违反里氏替换原则的解决方案的相关文章

设计模式原则之里氏替换原则

里氏替换原则,OCP作为OO的高层原则,主张使用“抽象(Abstraction)”和“多态(Polymorphism)”将设计中的静态结构改为动态结构,维持设计的封闭性.“抽象”是语言提供的功能.“多态”由继承语义实现. 定义1:如果对每一个类型为T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 如

设计模式六大原则(2):里氏替换原则

里氏替换原则 肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由

设计模式六大原则(2):里氏替换原则(转载)

肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需

设计模式六大原则(2):里氏替换原则(Liskov Substitution Principle)

肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需

设计原则(二)里氏替换原则(LSP)

一.什么是里氏替换原则 里氏替换原则的严格表达是: 如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型. 换言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别. 比如,假设有两个类,一个是Base类,另一个是Child类,并且Child类是Base的子类.那么一个方法如果可以接受一个基类对象b的话:method1(Ba

设计模式六大原则之二:里氏替换原则

定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成.新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障. 解决方

[转]设计模式六大原则[2]:里氏替换原则

肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类A完成.现需

设计模式六大原则(2)--里氏替换原则

定义: 程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换,也就是说所有引用基类的地方必须能透明地使用其子类的对象.通俗的来说,子类可以扩展父类的功能,但不能改变父类原有的功能. 由来: 第一次看见这个里氏替换原则的名字会觉着很奇特,根据以往的经验这一看就是外国友人首先提出的概念,然后便以她的姓命名该原则.确实是这样,它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出.里氏替换原则英文名称为Liskov Substi

六大原则之里氏替换原则

阐述一下: 肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型. 定义2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来:有一功能P1,由类