六大設(shè)計原則之里氏替換原則

里氏替換原則(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圖:


LSP.jpg

繼承作為面向?qū)ο蟮闹匾卣鳎m然給程序開發(fā)帶來了非常大的便利,但也引入了一些弊端。繼承的開發(fā)方式會給代碼帶來侵入性,可移植能力降低,類之間的耦合度較高。當(dāng)對父類修改時,就要考慮一整套子類的實(shí)現(xiàn)是否有風(fēng)險,測試成本較高。

里氏替換原則的目的是使用約定的方式,讓使用繼承后的代碼具備良好的擴(kuò)展性和兼容性。

使用了繼承,就一定要遵從里氏替換原則,否則會讓代碼出現(xiàn)問題的概率變得更大。

在設(shè)計模式中體現(xiàn)里氏替換原則的有如下幾個模式:

  • 策略模式
  • 組合模式
  • 代理模式
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容