里氏替換原則(Liskov Substitution Principle,LSP)是由麻省理工學(xué)院計算機(jī)科學(xué)系教授芭芭拉·利斯科夫(Barbara Liskov)于 1987 年在“面向?qū)ο蠹夹g(shù)的高峰會議”(OOPSLA)上發(fā)表的一篇文章《數(shù)據(jù)抽象和層次》(Data Abstractionand Hierarchy)里提出的,她提出:繼承必須確保超類所擁有的性質(zhì)在子類中仍然成立。
如果S是T的子類型,那么所有T類型的對象都可以在不破壞程序的情況下被S類型的對象替換。
簡單來說,子類可以擴(kuò)展父類的功能,但不能改變父類原有的功能。也就是說:當(dāng)子類繼承父類時,除添加新的方法且完成新增功能外,盡量不要重寫父類的方法。這句話包括了四點(diǎn)含義(非常重要):
- 子類可以實(shí)現(xiàn)父類的抽象方法,但不能覆蓋父類的非抽象方法。
- 子類可以增加自己特有的方法。
- 當(dāng)子類的方法重載父類的方法時,方法的前置條件(即方法的輸入?yún)?shù))要比父類的方法更寬松。
- 當(dāng)子類的方法實(shí)現(xiàn)父類的方法(重寫、重載或?qū)崿F(xiàn)抽象方法)時,方法的后置條件(即方法的輸出或返回值)要比父類的方法更嚴(yán)格或與父類的方法相等。
里氏替換原則的作用
·里氏替換原則是實(shí)現(xiàn)開閉原則的重要方式之一。
·解決了繼承中重寫父類造成的可復(fù)用性變差的問題。
·是動作正確性的保證,即類的擴(kuò)展不會給已有的系統(tǒng)引入新的錯誤,降低了代碼出錯的可能性。
·加強(qiáng)程序的健壯性,同時變更時可以做到非常好的兼容性,提高程序的維護(hù)性、可擴(kuò)展性,降低需求變更時引入的風(fēng)險。
關(guān)于里氏替換的場景,最有名的就是“正方形不是長方形”。同時還有一些關(guān)于動物的例子,比如鴕鳥、企鵝都是鳥,但是卻不能飛。這樣的例子可以非常形象地幫助我們理解里氏替換中關(guān)于兩個類的繼承不能破壞原有特性的含義。
舉例:
這里用不同種類的銀行卡作為場景對象進(jìn)行學(xué)習(xí)。儲蓄卡和信用卡都具備一定的消費(fèi)功能,但又有一些不同。例如信用卡不宜提現(xiàn),如果提現(xiàn)可能會產(chǎn)生高額的利息。構(gòu)建這樣一個模擬場景,假設(shè)在構(gòu)建銀行系統(tǒng)時,儲蓄卡是第一個類,信用卡是第二個類。為了讓信用卡可以使用儲蓄卡的一些方法,選擇由信用卡類繼承儲蓄卡類
違背原則的方案:
儲蓄卡類
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class CashCard {
/**提現(xiàn)*/
public String withdrawal(String orderId, BigDecimal amount){
System.out.println("提現(xiàn)成功,單號:"+orderId+", 金額:"+amount);
return "00001";
}
/**儲蓄*/
public String recharge(String orderId, BigDecimal amount){
System.out.println("儲蓄成功,單號:"+orderId+", 金額:"+amount);
return "00001";
}
/**
* 查詢交易流水
*/
public List<String> tradeFlow() {
List<String> tradeList = new ArrayList<String>();
tradeList.add("orderid:103423,amount:100000");
tradeList.add("orderid:103425,amount:150000");
tradeList.add("orderid:103428,amount:5050000");
return tradeList;
}
}
信用卡類:
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class CreditCard extends CashCard{
/**提現(xiàn)*/
@Override
public String withdrawal(String orderId, BigDecimal amount){
//信用卡對于提現(xiàn)有不同的邏輯
if(amount.compareTo(new BigDecimal(2000)) >= 0){
System.out.println("貸款金額校驗(限額1000),單號:"+orderId+",金額:"+amount);
return "4582342";
}
System.out.println("生成貸款單,單號:"+orderId+",金額:"+amount);
System.out.println("貸款成功,單號:"+orderId+",金額:"+amount);
return "00001";
}
/**信息卡儲蓄,相當(dāng)于還款,邏輯也完全不一樣*/
@Override
public String recharge(String orderId, BigDecimal amount){
System.out.println("生成還款單,單號:"+orderId+", 金額:"+amount);
System.out.println("還款成功,單號:"+orderId+", 金額:"+amount);
return "00001";
}
/**
* 查詢交易流水,可以直接使用父類邏輯
*/
public List<String> tradeFlow() {
return super.tradeFlow();
}
}
信用卡的功能實(shí)現(xiàn)是在繼承了儲蓄卡類后,進(jìn)行方法重寫:支付withdrawal()、還款recharge()。其實(shí)交易流水可以復(fù)用,也可以不用重寫這個類。這種繼承父類方式的優(yōu)點(diǎn)是復(fù)用了父類的核心功能邏輯,但是也破壞了原有的方法。此時繼承父類實(shí)現(xiàn)的信用卡類并不滿足里氏替換原則,也就是說,此時的子類不能承擔(dān)原父類的功能,直接當(dāng)作儲蓄卡使用。
里氏替換原則改善代碼:
儲蓄卡和信用卡在功能使用上有些許類似,在實(shí)際的開發(fā)過程中也有很多共同可復(fù)用的屬性及邏輯。實(shí)現(xiàn)這樣的類的最好方式是提取出一個抽象類,由抽象類定義所有卡的共用核心屬性、邏輯,把卡的支付和還款等動作抽象成正向和逆向操作。
抽象出銀行卡類:
public abstract class BankCard {
private String cardNo; //銀行卡都有卡號屬性
private String createDate; //銀行卡都有開卡時間
public BankCard(String cardNo, String createDate){
this.cardNo = cardNo;
this.createDate = createDate;
}
protected abstract boolean rule(BigDecimal amount);
//正向入賬,加錢
public String positive(String orderId, BigDecimal amount){
System.out.println("入款成功,卡號:"+cardNo+",單號:"+orderId+", 金額:"+amount);
return "00001";
}
//逆向入賬,減錢
public String negative(String orderId, BigDecimal amount){
System.out.println("出款成功,卡號:"+cardNo+",單號:"+orderId+", 金額:"+amount);
return "00001";
}
/**
* 查詢交易流水
*/
public List<String> tradeFlow() {
List<String> tradeList = new ArrayList<String>();
tradeList.add("orderid:103423,amount:100000");
tradeList.add("orderid:103425,amount:150000");
tradeList.add("orderid:103428,amount:5050000");
return tradeList;
}
public String getCardNo() {
return cardNo;
}
public String getCreateDate() {
return createDate;
}
}
儲蓄卡類實(shí)現(xiàn):
import java.math.BigDecimal;
public class CashCard extends BankCard {
public CashCard(String cardNo, String createDate){
super(cardNo,createDate);
}
/**規(guī)則過濾,儲蓄卡默認(rèn)通過*/
@Override
protected boolean rule(BigDecimal amount) {
return true;
}
/**提現(xiàn)*/
public String withdrawal(String orderId, BigDecimal amount){
System.out.println("提現(xiàn)成功,單號:"+orderId+", 金額:"+amount);
return super.negative(orderId,amount);
}
/**儲蓄*/
public String recharge(String orderId, BigDecimal amount){
System.out.println("儲蓄成功,單號:"+orderId+", 金額:"+amount);
return super.positive(orderId,amount);
}
public boolean risk(String cardNo,String orderId,BigDecimal amount){
System.out.println("風(fēng)險檢測,卡號:"+cardNo+",單號:"+orderId+",金額:"+amount);
return true;
}
}
儲蓄卡類中繼承抽象銀行卡父類 BankCard,實(shí)現(xiàn)的核心功能包括規(guī)則過濾rule、提現(xiàn)withdrawal、儲蓄recharge和新增的擴(kuò)展方法,即風(fēng)控校驗 checkRisk。這樣的實(shí)現(xiàn)方式滿足了里氏替換的基本原則,既實(shí)現(xiàn)抽象類的抽象方法,又沒有破壞父類中的原有方法。
信用卡類實(shí)現(xiàn):
import java.math.BigDecimal;
public class CreditCard extends CashCard{
public CreditCard(String cardNo, String orderId){
super(cardNo,orderId);
}
boolean rule2(BigDecimal amount){
return amount.compareTo(new BigDecimal(2000)) <= 0;
}
/**
* 貸款,信用卡提現(xiàn)
*/
public String loan(String orderId, BigDecimal amount){
boolean rule = rule2(amount);
if(!rule){
System.out.println("貸款失敗??!單號:"+orderId+",金額:"+amount);
return "00002";
}
System.out.println("生成貸款單,單號:"+orderId+",金額:"+amount);
System.out.println("貸款成功,單號:"+orderId+",金額:"+amount);
return super.negative(orderId,amount);
}
/**
* 還款,信用卡還款
*/
public String repayment(String orderId, BigDecimal amount){
System.out.println("生成還款單,單號:"+orderId+",金額:"+amount);
System.out.println("還款成功,單號:"+orderId+",金額:"+amount);
return super.positive(orderId,amount);
}
}
信用卡類在繼承父類后,使用了公用的屬性,即卡號 cardNo、開卡時間createDate,同時新增了符合信用卡功能的新方法,即貸款loan、還款repayment,并在兩個方法中都使用了抽象類的核心功能。關(guān)于儲蓄卡中的規(guī)則校驗方法,新增了自己的規(guī)則方法 rule2,并沒有破壞儲蓄卡中的校驗方法。子類隨時可以替代儲蓄卡類。信用卡類具備儲蓄卡的所有功能
UML圖:

繼承作為面向?qū)ο蟮闹匾卣鳎m然給程序開發(fā)帶來了非常大的便利,但也引入了一些弊端。繼承的開發(fā)方式會給代碼帶來侵入性,可移植能力降低,類之間的耦合度較高。當(dāng)對父類修改時,就要考慮一整套子類的實(shí)現(xiàn)是否有風(fēng)險,測試成本較高。
里氏替換原則的目的是使用約定的方式,讓使用繼承后的代碼具備良好的擴(kuò)展性和兼容性。
使用了繼承,就一定要遵從里氏替換原則,否則會讓代碼出現(xiàn)問題的概率變得更大。
在設(shè)計模式中體現(xiàn)里氏替換原則的有如下幾個模式:
- 策略模式
- 組合模式
- 代理模式