軟體架構設計原則之里氏替換原則
里氏替換原則(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彈架構”可獲取更多技術乾貨!