1. 程式人生 > 其它 >軟體架構設計原則之里氏替換原則

軟體架構設計原則之里氏替換原則


里氏替換原則(Liskov Substitution Principle,LSP)是指如果對每一個型別為T1的物件o1,都有型別為T2的物件O2,使得以T1定義的所有程式P在所有的物件O1都替換成O2時,程式P的行為沒有發生變化,那麼型別T2是型別T1的子型別。

這個定義看上去還是比較抽象的,我們重新理解一下。可以理解為一個軟體實體如果適用於一個父類,那麼一定適用於其子類,所有引用父類的地方必須能透明地使用其子類的物件,子類物件能夠替換父類物件,而程式邏輯不變。根據這個理解,引申含義為:子類可以擴充套件父類的功能,但不能改變父類原有的功能。

(1)子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。

(2)子類可以增加自己特有的方法。

(3)當子類的方法過載父類的方法時,方法的前置條件(即方法的輸入/入參)要比父類方法的輸入引數更寬鬆。

(4)當子類的方法實現父類的方法時(重寫/過載或實現抽象方法),方法的後置條件(即方法的輸出/返回值)要比父類更嚴格或與父類一樣。

在講開閉原則的時候我埋下了一個伏筆,在獲取折扣時重寫覆蓋了父類的getPrice()方法,增加了一個獲取原始碼的方法getOriginPrice(),顯然就違背了里氏替換原則。我們修改一下程式碼,不應該覆蓋getPrice()方法,增加getDiscountPrice()方法:

public classJavaDiscountCourse extendsJavaCourse {

publicJavaDiscountCourse(Integer id, String name, Double price) {

super(id, name, price);

}

public Double getDiscountPrice(){

returnsuper.getPrice() * 0.61;

}

}

使用里氏替換原則有以下優點:

(1)約束繼承氾濫,是開閉原則的一種體現。

(2)加強程式的健壯性,同時變更時也可以做到非常好的相容性,提高程式的可維護性和擴充套件性,降低需求變更時引入的風險。

現在來描述一個經典的業務場景,用正方形、矩形和四邊形的關係說明裡氏替換原則,我們都知道正方形是一個特殊的長方形,所以就可以建立一個父類Rectangle:

public classRectangle {

private longheight;

private longwidth;

@Override

public longgetWidth() {

returnwidth;

}

@Override

public longgetLength() {

returnlength;

}

public voidsetLength(longlength) {

this.length = length;

}

public voidsetWidth(longwidth) {

this.width = width;

}

}

建立正方形類Square繼承Rectangle類:

public classSquare extendsRectangle {

private longlength;

public longgetLength() {

returnlength;

}

public voidsetLength(longlength) {

this.length = length;

}

@Override

public longgetWidth() {

returngetLength();

}

@Override

public longgetHeight() {

returngetLength();

}

@Override

public voidsetHeight(longheight) {

setLength(height);

}

@Override

public voidsetWidth(longwidth) {

setLength(width);

}

}

在測試類中建立resize()方法,長方形的寬應該大於等於高,我們讓高一直自增,直到高等於寬,變成正方形:

public static voidresize(Rectangle rectangle){

while(rectangle.getWidth() >= rectangle.getHeight()){

rectangle.setHeight(rectangle.getHeight() + 1);

System.out.println("width:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());

}

System.out.println("resize方法結束" +

"\nwidth:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());

}

測試程式碼如下:

public static voidmain(String[] args) {

Rectangle rectangle = newRectangle();

rectangle.setWidth(20);

rectangle.setHeight(10);

resize(rectangle);

}

執行結果如下圖所示。

我們發現高比寬還大了,這在長方形中是一種非常正常的情況。現在我們把Rectangle類替換成它的子類Square,修改測試程式碼:

public static voidmain(String[] args) {

Square square = newSquare();

square.setLength(10);

resize(square);

}

上述程式碼執行時出現了死迴圈,違背了里氏替換原則,將父類替換為子類後,程式執行結果沒有達到預期。因此,我們的程式碼設計是存在一定風險的。里氏替換原則只存在於父類與子類之間,約束繼承氾濫。我們再來建立一個基於長方形與正方形共同的抽象四邊形介面Quadrangle:

public interfaceQuadrangle {

longgetWidth();

longgetHeight();

}

修改長方形類Rectangle:

public classRectangle implementsQuadrangle {

private longheight;

private longwidth;

@Override

public longgetWidth() {

returnwidth;

}

public longgetHeight() {

returnheight;

}

public voidsetHeight(long height) {

this.height = height;

}

public voidsetWidth(longwidth) {

this.width = width;

}

}

修改正方形類Square:

public classSquare implementsQuadrangle {

private longlength;

public longgetLength() {

returnlength;

}

public voidsetLength(longlength) {

this.length = length;

}

@Override

public long getWidth() {

returnlength;

}

@Override

public longgetHeight() {

returnlength;

}

}

此時,如果我們把resize()方法的引數換成四邊形介面Quadrangle,方法內部就會報錯。因為正方形類Square已經沒有了setWidth()和setHeight()方法。因此,為了約束繼承氾濫,resize()方法的引數只能用Rectangle類。當然,我們在後面的設計模式的內容中還會繼續深入講解。

小測一下

本文為“Tom彈架構”原創,轉載請註明出處。技術在於分享,我分享我快樂!
如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支援是我堅持創作的動力。關注微信公眾號“Tom彈架構”可獲取更多技術乾貨!