[OOD]違反里氏替換原則的解決方案

關於OOD中的里氏替換原則,大家耳熟能祥了,不再展開,可以參考設計模式的六大設計原則之里氏替換原則。這裏嘗試討論常常違反的兩種形式和解決方案。

違反里氏替換原則的根源是對子類及父類關係不明確。我們在設計繼承關係常常受一些主觀認識的左右,比如Robert C. Martin提到的線段與線的關係,以及被大家說到爛的正方形與矩形。從以前的經驗我們認爲它們符合繼承關係,比如線段是線的較短形式,正方形是矩形的一個特例。但事實上它們並不能完全的包容和替代。

以集合的形式表示,左圖是里氏替換的目標,子類可以完全包容了父類的特性集合。右圖則是說兩者存在不兼容的特性集合:
LRP

對應的解決方案就是進一步抽象,將它們之前的關係從語言的角度重新定義,也許果真是is-a, 也許是has-a,也許它們只是兄弟。
基本的思路如下:

1. 找到更高層次的抽象

up_abstraction
以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 (由誰實現)的情況,不如轉化爲組合模式,如下面的關係:
compositor
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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章