1. 程式人生 > >OO設計原則 -- Liskov Substitution Principle: 里氏替換原則(LSP)

OO設計原則 -- Liskov Substitution Principle: 里氏替換原則(LSP)

概要

 Functions that use pointers or references to base classesmust be able to use objects of derived classes without knowing it.
所有引用基類的地方必須能透明地使用其子類的物件。

 即:
◇ 所以使用基類程式碼的地方,用派生類程式碼替換後,能夠正確的執行動作處理。

◇ 換句話說,如果派生類替換了基類後,不能夠正確執行動作,那麼他們的繼承關係就應該廢除。

 換個說法,只有滿足以下2個條件的OO設計才可被認為是滿足了LSP原則

◇ 不應該在程式碼中出現if/else之類對子類型別進行判斷的條件。

以下程式碼就違反了LSP定義。

 if (obj typeof Class1) {
     do something
 } else if (obj typeof Class2) {
     do something else
 }

◇ 子類應當可以替換父類並出現在父類能夠出現的任何地方,

      或者說如果我們把程式碼中使用基類的地方用它的子類所代替,程式碼還能正常工作。

里氏替換原則LSP是使程式碼符合開閉原則的一個重要保證。同時LSP體現了:

◇ 類的繼承原則:如果一個繼承類的物件可能會在基類出現的地方出現執行錯誤,

       則該子類不應該從該基類繼承,或者說,應該重新設計它們之間的關係。

◇ 動作正確性保證:從另一個側面上保證了符合LSP設計原則的類的擴充套件不會給已有的系統引入新的錯誤。

類的繼承原則:

Robert C. Martin氏在介紹Liskov Substitution Principle (LSP)的原文裡,舉了Rectangle和Square的例子。

這裡沿用這個例子,但用Java語言對其加以重寫,並忽略了某些細節只列出下面的精要

部分來說明 里氏替換原則 對類的繼承上的約束。

//程式碼:
 
class Rectangle {
    double width;
    doubleheight;
    
    
    public double getHeight() {
        returnheight;
    }
    public void setHeight(doubleheight) {
        this.height = height;
    }
    public double getWidth() {
        returnwidth;
    }
    public void setWidth(doublewidth) {
        this.width = width;
    }  
}
 
class Square extends Rectangle {
    public void setHeight(doubleheight) {
        super.setHeight(height);
        super.setWidth(height);
    }
    
    public void setWidth(doublewidth) {
        super.setHeight(width);
        super.setWidth(width);
    }
}

這裡Rectangle是基類,Square從Rectangle繼承。
這種繼承關係有什麼問題嗎?

假如已有的系統中存在以下既有的業務邏輯程式碼:
void g(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    if (r.getWidth() * r.getHeight() != 20) {
        throw new RuntimeException();
    }
}

則對應於擴充套件類Square,在呼叫既有業務邏輯時:
   Rectangle square = newSquare();
   g(square);

會丟擲一個RuntimeException異常。這顯然違反了LSP原則。


動作正確性保證:

因為LSP對子類的約束,所以為已存在的類做擴充套件構造一個新的子類時,

根據LSP的定義,不會給已有的系統引入新的錯誤。

Design by Contract

根據Bertrand Meyer氏提出的Design by Contract(DBC:基於合同的設計)概念的描述,

對於類的一個方法,都有一個前提條件以及一個後續條件,前提條件說明方法接受什麼樣的引數資料等,

只有前提條件得到滿足時,這個方法才能被呼叫;同時後續條件用來說明這個方法完成時的狀態,

如果一個方法的執行會導致這個方法的後續條件不成立,那麼 這個方法也不應該正常返回。

現在把前提條件以及後續條件應用到繼承子類中,子類方法應該滿足:

1)前提條件不強於基類.

2)後續條件不弱於基類.

換 句話說,通過基類的介面呼叫一個物件時,使用者只知道基類前提條件以及後續條件。

因此繼承類不得要求使用者提供比基類方法要求的更強的前提條件,

亦即,繼承類方法必須接受任何基類方法能接受的任何條件(引數)。

同樣,繼承類必須順從基類的所有後續條件,亦即,

繼承類方法的行為和輸出不得違反由基類建立起來的任 何約束,不能讓使用者對繼承類方法的輸出感到困惑。

這樣,我們就有了基於合同的LSP,基於合同的LSP是LSP的一種強化。

在很多情況下,在設計初期我們類之間的關係不是很明確,

LSP則給了我們一個判斷和設計類之間關係的基準:需不需要繼承,以及怎樣設計繼承關係。

總結

LSP: 子類必須能夠替換基類。

Subtypes must besubstitutable  for their base types.

 1.      LSP關注的是怎樣良好的使用繼承.

 2.      必須要清楚是使用一個Method還是要擴充套件它,但是絕對不是改變它。

 3.      LSP清晰的指出,OOD的IS-A關係是就行為方式而言,

          行為方式是可以進行合理假設的,是客戶程式所依賴的。

 4.      LSP讓我們得出一個重要的結論:一個模型如果孤立的看,並不具有真正意義的有效性。

          模型的有效性只能通過它的客戶程式來表現。必須根據設計的使用者做出的合理假設來審視它。

           而假設是難以預測的,直到設計臭味出現的時候才處理它們。

 5.      對於LSP的違反也潛在的違反了OCP