閱讀818 返回首頁    go 阿裏雲 go 技術社區[雲棲]


連載:麵向對象葵花寶典:思想、技巧與實踐(32) - LSP原則

LSP是唯一一個以人名命名的設計原則,而且作者還是一個“女博士” 大笑

=============================================================


LSP,Liskov substitution principle,中文翻譯為“裏氏替換原則”。

 

這是麵向對象原則中唯一一個以人名命名的原則,雖然Liskov在中國的知名度沒有UNIX的幾位巨匠(Kenneth Thompson、Dennis Ritchie)、GOF四人幫那麼響亮,但查一下資料,你會發現其實Liskov也是非常牛的:2008年圖靈獎獲得者,曆史上第一個女性計算機博士學位獲得者。其詳細資料可以在維基百科上查閱:https://en.wikipedia.org/wiki/Barbara_Liskov 

 

言歸正傳,我們來看看LSP原則到底是怎麼一回事。

LSP最原始的解釋當然來源於Liskov女士了,她在1987年的OOPSLA大會上提出了LSP原則,1988年,她將文章發表在ACM的SIGPLAN Notices雜誌上,其中詳細解釋了LSP原則:

A type hierarchy is composed of subtypes and supertypes. The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra.What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

 

英文比較長,看起來比較累,我們簡單的翻譯並歸納一下:

1) 子類的對象提供了父類的所有行為,且加上子類額外的一些東西(可以是功能,也可以是屬性);

2) 當程序基於父類實現時,如果將子類替換父類而程序不需要修改,則說明符合LSP原則

 

雖然我們稍微翻譯和整理了一下,但實際上還是很拗口和難以理解。

幸好還有Martin大師也覺得這個不怎麼通俗易懂,Robert Martin在1996年為《C++ Reporter》寫了一篇題為《The The Liskov Substitution Principle》的文章,解釋如下:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

翻譯一下就是:函數使用指向父類的指針或者引用時,必須能夠在不知道子類類型的情況下使用子類的對象

 

Martin大師解釋了一下,相對容易理解多了。但Martin大師還不滿足,在2002年,Martin在他出版的《Agile   Software   Development   Principles   Patterns   and   Practices》一書中,又進一步簡化為:

Subtypes   must   be   substitutable   for   their   base   types。

翻譯一下就是:子類必須能替換成它們的父類

 

經過Martin大師的兩次翻譯,我相信LSP原則本身已經解釋得比較容易理解了,但問題的關鍵是:如何滿足LSP原則?或者更通俗的講:什麼情況下子類才能替換父類?

 

我們知道,對於調用者來說(Liskov解釋中提到的P),和父類交互無非就是兩部分:調用父類的方法、得到父類方法的輸出,中間的處理過程,P是無法知道的。

 

也就是說,調用者和父類之間的聯係體現在兩方麵:函數輸入,函數輸出。詳細如下圖:

 

有了這個圖之後,如何做到LSP原則就清晰了:

1) 子類必須實現或者繼承父類所有的公有函數,否則調用者調用了一個父類中有的函數,而子類中沒有,運行時就會出錯;

2) 子類每個函數的輸入參數必須和父類一樣,否則調用父類的代碼不能調用子類;

3) 子類每個函數的輸出(返回值、修改全局變量、插入數據庫、發送網絡數據等)必須不比父類少,否則基於父類的輸出做的處理就沒法完成。

 

有了這三條原則後,就可以很方便的判斷類設計是否符合LSP原則了。需要注意的是第3條的關鍵是“不比父類少”,也就是說可以比父類多,即:父類的輸出是子類輸出的子集

 

有的朋友看到這三條原則可能有點納悶:這三條原則一出,那子類還有什麼區別哦,豈不都是一樣的實現了,那還會有不同的子類麼?

 

其實如果仔細研究這三條原則,就會發現其中隻是約定了輸入輸出,而並沒有約束中間的處理過程。例如:同樣一個寫數據庫的輸出,A類可以是讀取XML數據然後寫入數據庫,B類可以是從其它數據庫讀取數據然後本地的數據庫,C類可以是通過分析業務日誌得到數據然後寫入數據庫。這3個類的處理過程都不一樣,但最後都寫入數據到數據庫了。

 

LSP原則最經典的例子就是“長方形和正方形”這個例子。從數學的角度來看,正方形是一種特殊的長方形,但從麵向對象的角度來觀察,正方形並不能作為長方形的一個子類。原因在於對於長方形來說,設定了寬高後,麵積 = 寬 * 高;但對於正方形來說,設定高同時就設定了寬,設定寬就同時設定了高,最後的麵積並不是等於我們設定的 寬 * 高,而是等於最後一次設定的寬或者高的平方。

具體代碼樣例如下:

Rectangle.java

package com.oo.java.principles.lsp;

/**
 * 長方形
 */
public class Rectangle {

    protected int _width;
    protected int _height;
    
    /**
     * 設定寬
     * @param width
     */
    public void setWidth(int width){
        this._width = width;
    }
    
    /**
     * 設定高
     * @param height
     */
    public void setHeight(int height){
        this._height = height;
    }
    
    /**
     * 獲取麵積
     * @return
     */
    public int getArea(){
        return this._width * this._height;
    }
}

Square.java

package com.oo.java.principles.lsp;

/**
 * 正方形
 */
public class Square extends Rectangle {
    
    /**
     * 設定“寬”,與長方形不同的是:設定了正方形的寬,同時就設定了正方形的高
     */
    public void setWidth(int width){
        this._width = width;
        this._height = width;
    }
    
    /**
     * 設定“高”,與長方形不同的是:設定了正方形的高,同時就設定了正方形的寬
     */
    public void setHeight(int height){
        this._width = height;
        this._height = height;
    }

}

UnitTester.java

package com.oo.java.principles.lsp;

public class UnitTester {

    public static void main(String[] args){
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(4);
        rectangle.setHeight(5);
        
        //如下assert判斷為true
        assert( rectangle.getArea() == 20);
        
        rectangle = new Square();
        rectangle.setWidth(4);
        rectangle.setHeight(5);
        
        //如下assert判斷為false,斷言失敗,拋出java.lang.AssertionError
        assert( rectangle.getArea() == 20);
    }
}

上麵這個樣例同時也給出了一個判斷子類是否符合LSP的取巧的方法,即:針對父類的單元測試用例,傳入子類是否也能夠測試通過。如果測試能夠通過,則說明符合LSP原則,否則就說明不符合LSP原則


================================================ 
轉載請注明出處:https://blog.csdn.net/yunhua_lee/article/details/26807601
================================================ 


最後更新:2017-04-03 08:26:15

  上一篇:go 請教如何實現SQL查詢24小時內,當前時間之前20分鍾的信息
  下一篇:go 機房收費係統之上下機