里氏替換原則(Liskov Substitution Principle,LSP)是指如果對每一個類型為T1的對象o1,都有類型為T2的對象O2,使得以T1定義的所有程序P在所有的對象O1都替換成O2時,程序P的行為沒有發生變化,那么類型T2是類型T1的子類型。
這個定義看上去還是比較抽象的,我們重新理解一下。可以理解為一個軟件實體如果適用於一個父類,那么一定適用於其子類,所有引用父類的地方必須能透明地使用其子類的對象,子類對象能夠替換父類對象,而程序邏輯不變。根據這個理解,引申含義為:子類可以擴展父類的功能,但不能改變父類原有的功能。
(1)子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
(2)子類可以增加自己特有的方法。
(3)當子類的方法重載父類的方法時,方法的前置條件(即方法的輸入/入參)要比父類方法的輸入參數更寬松。
(4)當子類的方法實現父類的方法時(重寫/重載或實現抽象方法),方法的后置條件(即方法的輸出/返回值)要比父類更嚴格或與父類一樣。
在講開閉原則的時候我埋下了一個伏筆,在獲取折扣時重寫覆蓋了父類的getPrice()方法,增加了一個獲取源碼的方法getOriginPrice(),顯然就違背了里氏替換原則。我們修改一下代碼,不應該覆蓋getPrice()方法,增加getDiscountPrice()方法:
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getDiscountPrice(){
return super.getPrice() * 0.61;
}
}
使用里氏替換原則有以下優點:
(1)約束繼承泛濫,是開閉原則的一種體現。
(2)加強程序的健壯性,同時變更時也可以做到非常好的兼容性,提高程序的可維護性和擴展性,降低需求變更時引入的風險。
現在來描述一個經典的業務場景,用正方形、矩形和四邊形的關系說明里氏替換原則,我們都知道正方形是一個特殊的長方形,所以就可以創建一個父類Rectangle:
public class Rectangle {
private long height;
private long width;
@Override
public long getWidth() {
return width;
}
@Override
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public void setWidth(long width) {
this.width = width;
}
}
創建正方形類Square繼承Rectangle類:
public class Square extends Rectangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
@Override
public long getWidth() {
return getLength();
}
@Override
public long getHeight() {
return getLength();
}
@Override
public void setHeight(long height) {
setLength(height);
}
@Override
public void setWidth(long width) {
setLength(width);
}
}
在測試類中創建resize()方法,長方形的寬應該大於等於高,我們讓高一直自增,直到高等於寬,變成正方形:
public static void resize(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 void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(20);
rectangle.setHeight(10);
resize(rectangle);
}
運行結果如下圖所示。
我們發現高比寬還大了,這在長方形中是一種非常正常的情況。現在我們把Rectangle類替換成它的子類Square,修改測試代碼:
public static void main(String[] args) {
Square square = new Square();
square.setLength(10);
resize(square);
}
上述代碼運行時出現了死循環,違背了里氏替換原則,將父類替換為子類后,程序運行結果沒有達到預期。因此,我們的代碼設計是存在一定風險的。里氏替換原則只存在於父類與子類之間,約束繼承泛濫。我們再來創建一個基於長方形與正方形共同的抽象四邊形接口Quadrangle:
public interface Quadrangle {
long getWidth();
long getHeight();
}
修改長方形類Rectangle:
public class Rectangle implements Quadrangle {
private long height;
private long width;
@Override
public long getWidth() {
return width;
}
public long getHeight() {
return height;
}
public void setHeight(long height) {
this.height = height;
}
public void setWidth(long width) {
this.width = width;
}
}
修改正方形類Square:
public class Square implements Quadrangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
@Override
public long getWidth() {
return length;
}
@Override
public long getHeight() {
return length;
}
}
此時,如果我們把resize()方法的參數換成四邊形接口Quadrangle,方法內部就會報錯。因為正方形類Square已經沒有了setWidth()和setHeight()方法。因此,為了約束繼承泛濫,resize()方法的參數只能用Rectangle類。當然,我們在后面的設計模式的內容中還會繼續深入講解。
關注微信公眾號『 Tom彈架構 』回復“設計模式”可獲取完整源碼。
本文為“Tom彈架構”原創,轉載請注明出處。技術在於分享,我分享我快樂!
如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支持是我堅持創作的動力。關注微信公眾號『 Tom彈架構 』可獲取更多技術干貨!
其他設計原則
Tom彈架構:開閉原則(Open-Closed Principle,OCP)
Tom彈架構:依賴倒置原則(Dependence Inversion Principle,DIP)
Tom彈架構:單一職責原則(Simple Responsibility Pinciple,SRP)
Tom彈架構:接口隔離原則(Interface Segregation Principle, ISP)