軟件設計原則----LisKov替換原則(LSP)

“一個軟件實體如果使用的是一個基類的話,一定適用於其子類,而且根本不能覺察出基類對象和子類對象的區別。”

陳述:

  • 子類型(Subtype)必須能夠替換他們的基類型(Basetype)
Barbara Liskov對原則的陳述:

若對每個類型S的對象o1,都存在一個類型T的對象o2,使得在所有針對T編寫的程序P中,用o1替換o2後,程序P的行爲功能不變,則S是T的子類型。

通俗地講,就是子類型能夠完全替換父類型,而不會讓調用父類型的客戶程序從行爲上有任何改變。

意義:

我們在客戶程序在調用某一個類時,實際上是對該類的整個繼承體系設定了一套約束,繼承體系中的所有類必須遵循這一約束,即前置條件和後置條件必須保持一致。這爲對象繼承加上了一把嚴格的枷鎖。顯然,LSP原則對於約束繼承的泛濫具有重要意義。

分析:
  • 違反這個職責將導致程序的脆弱性和對OCP的違反
例如:基類Base,派生類Derived,派生類實例d,函數f(Base* p);
    • f(&d) 會導致錯誤
                  顯然D對於f是脆弱的。
    • 如果我們試圖編寫一些測試,以保證把d傳給f時可以使f具有正確的行爲。那麼這個測試違反了OCP——因爲f無法對Base的所有派生類都是封閉的。
經典例子:長方形與正方形駁論

class Rectangle
{
private:
     long width;
     long height;
public:
    void setWidth(long width)
    {
        this->width = width;
    }
    long getWidth()
    {
        return this->width;
    }
    void setHeight(long height)
    {
        this->height = height;
    }
    long getHeight()
    {
        return this->height;
    }
};

//正方形類
class Square
{
private:
	long side;
    
public:
	void setSide(long side)
    {
        this->side = side;
    }
    long getSide()
    {
        return side;
    }
};
//正方形類(如果繼承自長方形類):
class Square : public Rectangle
{
private:
	long side;
public:
	void setWidth(long width)
    {
        setSide(width);
    }
    long getWidth()
    {
        return getSide();
    }
    void setHeight(long height)
    {
        setSide(height);
    }
    long getHeight()
    {
        return getSide();
    }
    long getSide()
    {
        return side;
    }
    void setSide(long side)
    {
        this->side = side;
    }
};

class SmartTest
{
public:
    void resize(Rectangle r)
    {
	while (r.getHeight() <= r.getWidth() )
        {
	    r.setWidth(r.getWidth() + 1);
        }
    }
};

從上面小函數可見,只想改變長方形的寬時,如果把正方形看成一種長方形的話,則正方形的長和寬都被改變了。LSP原則被破壞了,Square不應成爲Rectangle的子類。
結論:
里氏代換與通常的數學法則和生活常識有不可混淆的區別。
考慮一個設計是否恰當時,不能孤立的看待並判斷,應該從此設計的使用者所作出的假設來審視它!

思考:
這個看似明顯正確的模型怎麼會出錯呢?
“正方形是一種長方形”
對不是SmartTest函數的編寫者而言,正方形可以是長方形,但是對SmartTest函數的編寫者而言,Square絕對不是Rectangle!!
OOD中對象之間是否存在IS-A關係,應該從行爲的角度來看待。
->而行爲可以依賴客戶程序做出合理的假設。

改進:
引入一個Quadrangle(四邊形)類,並將Rectangle 與Square變成它的具體子類,解決了Rectangle 與Square的關係不符合里氏替換原則的問題。
class Quadrangle
{
public:
	virtual long getWidth() = 0;	
	virtual long getHeight() = 0;
};
 Quadrangle類只聲明兩個取值方法,不聲明任何的賦值方法。
//長方形類:
class Rectangle : public Quadrangle 
{
private:
	long width;
    long height;
    
public:
	void setWidth(long width)
    {
        this->width = width;
    }
    long getWidth()
    {
        return this->width;
    }
    void setHeight(long height)
    {
        this->height = height;
    }
    long getHeight()
    {
        return this->height;
    }
};

//正方形類:
class Square : public Quadrangle 
{
private:
	long side;
	
public:
	void setSide(long side)
    {
		this->side = side;
    }
	
    long getSide()
    {
        return side;
    }
	
    long getWidth()
    {
        return getSide();
    }
	
    long getHeight()
    {
        return getSide();
    }
};
問題如何得以避免?
基類Quadrangle類沒有賦值方法,因此類似於 SmartTest的resize()方法不可能適用於Quadrangle類型,而只能適用於不同的具體子類Rectangle 和Square,因此里氏替換原則不可能被破壞。
結論:
  • 儘量從抽象類繼承,而不從具體類繼承。
  • 如果有兩個具體類A和B有繼承關係,那麼一個最簡單的修改方案應當是建立一個抽象類C,讓類A和B成爲抽象類C的子類。
  • 更進一步:  如果有一個由繼承關係形成的等級結構的話,那麼在等級結構的樹圖上面所有的樹葉節點都應該是具體類,而所有的樹枝節點都應該是抽象類或接口。

相應設計模式:
  • Strategy
  • Composite
  • Proxy 

參考資源:

《設計模式:可複用面向對象軟件的基礎》,ERICH GAMMA RICHARD HELM RALPH JOHNSON JOHN VLISSIDES著作,李英軍 馬曉星 蔡敏 劉建中譯,機械工業出版社,2005.6

《敏捷軟件開發:原則、模式與實踐》,Robert C. Martin著,鄧輝譯,清華大學出版社,2003.9

《設計模式解析》,Alan Shalloway等著(徐言聲譯),人民郵電出版社,2006.10



發佈了90 篇原創文章 · 獲贊 110 · 訪問量 69萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章