嘻哈說:設(shè)計(jì)模式之里氏替換原則

image

1、定義

按照慣例,首先我們來看一下里氏替換原則的定義。

所有引用基類(父類)的地方必須能透明地使用其子類的對象。
通俗的說,子類可以擴(kuò)展父類功能,但不能改變父類原有功能。

核心思想是繼承。 通過繼承,引用基類的地方就可以使用其子類的對象了。例如:

Parent parent = new Child();

重點(diǎn)來了,那么如何透明地使用呢?

我們來思考個(gè)問題,子類可以改變父類的原有功能嗎?

public class Parent {
    public int add(int a, int b){
        return a+b;
    }
}

public class Child extends Parent{
    @Override
    public int add(int a, int b) {
        return a-b;
    }
}

這樣好不好?

肯定是不好的,本來是加法卻修改成了減法,這顯然是不符合認(rèn)知的。

它違背了里氏替換原則,子類改變了父類原有功能后,當(dāng)我們在引用父類的地方使用其子類的時(shí)候,沒辦法透明使用add方法了。

父類中凡是已經(jīng)實(shí)現(xiàn)好的方法,實(shí)際上是在設(shè)定一系列的規(guī)范和契約,雖然它不強(qiáng)制要求所有的子類必須遵從這些規(guī)范,但是如果子類對這些非抽象方法任意修改,就會(huì)對整個(gè)繼承體系造成破壞。

所以,透明使用的關(guān)鍵就是,子類不能改變父類原有功能。

2、含義

1、子類可以實(shí)現(xiàn)父類的抽象方法,但是不能覆蓋父類的非抽象方法。

剛才我們已經(jīng)說過,子類不能改變父類的原有功能,所以子類不能覆蓋父類的非抽象方法。

子類可以實(shí)現(xiàn)父類的抽象方法,must be,抽象方法本來就是讓子類實(shí)現(xiàn)的。

package com.fanqiekt.principle.liskov.rapper;

/**
 * Rapper抽象類
 *
 * @Author: 番茄課堂-懶人
 */
public abstract class BaseRapper {

    /**
     * freeStyle
     */
    protected abstract void freeStyle();

    /**
     * 播放伴奏
     */
    protected void playBeat(){
        System.out.println("從樂庫中隨機(jī)播放一首伴奏:動(dòng)次打次...");
    }

    /**
     * 表演
     * 播放伴奏,并進(jìn)行freeStyle
     */
    public void perform(){
        playBeat();
        freeStyle();
    }
}

BaseRapper是一個(gè)抽象類,它代表著Rapper的基類。

Rapper一般的表演方式是隨機(jī)播放一首伴奏然后進(jìn)行free style。

freeStyle則各有各的不同,所以將它寫成了一個(gè)抽象方法,讓子類自由發(fā)揮。

playBeat流程大多是一樣的,從樂庫中隨意播放伴奏,所以將它寫成了一個(gè)非抽象方法。

perform的流程大多也是一樣的,放伴奏,然后freestyle,也將它寫成了非抽象方法。

package com.fanqiekt.principle.liskov.rapper;

/**
 * Rapper
 *
 * @author 番茄課堂-懶人
 */
public class Rapper extends BaseRapper {

    /**
     * 播放伴奏
     *
     * 子類覆蓋父類非抽象方法
     */
    @Override
    protected void playBeat() {
        System.out.println("關(guān)閉麥克風(fēng)");
    }

    /**
     * 表演
     *
     * 子類覆蓋父類非抽象方法
     */
    @Override
    public void perform() {
        System.out.println("跳鬼步");

    }

    /**
     * 子類可以覆蓋父類抽象方法
     */
    @Override
    protected void freeStyle() {
        System.out.println("藥藥切克鬧,煎餅果子來一套!");
    }
}

Rapper是BaseRapper的子類,覆蓋了父類的抽象方法freeStyle。

覆蓋了父類的非抽象方法playBeat,并將邏輯更改為打開麥克風(fēng),明顯違背了里氏替換原則。
這顯然是非常錯(cuò)誤的寫法, 原因是父類行為與子類行為不一致,不可以透明的使用父類了。
播放伴奏你卻給我打開麥克風(fēng),你確定不是在逗我?

我嘗試著將playBeat進(jìn)行下修改。

/**
 * 子類覆蓋父類非抽象方法
 * 子類方法中調(diào)用super方法
 */
@Override
protected void playBeat() {
    super.playBeat();
    System.out.println("關(guān)閉麥克風(fēng)");
}

在子類方法中調(diào)用super方法,這樣修改是否可以?

不可以,原因是打開麥克風(fēng)跟播放伴奏沒有任何邏輯上的關(guān)系。

透明使用子類的時(shí)候,雖然伴奏也會(huì)正常的播放,但卻在調(diào)用者不知情的情況下關(guān)閉了麥克風(fēng),而關(guān)閉麥克風(fēng)又明顯與播放伴奏無關(guān)。
這就對于調(diào)用者無法做到真正的透明了。

同樣覆蓋了父類的非抽象方法perform,并將邏輯更改為跳舞,這要是違背了里氏替換原則的。
只跳舞不說唱的表演還叫Rapper嗎?

我嘗試著將perform進(jìn)行下修改。

/**
 * 表演
 * freestyle + 跳舞
 * 子類覆蓋父類非抽象方法
 */
@Override
public void perform() {
    super.perform();
    System.out.println("跳鬼步");
}

perform方法我這樣修改可以嗎?

這個(gè)倒是可以的,為什么同樣是子類調(diào)用super方法,為什么playBeat不可以,perform就可以呢?

perform是表演,跳舞是表演的一種補(bǔ)充,屬于表演范疇,調(diào)用者可以透明地調(diào)用perform方法。

安靜的freestyle還是手舞足蹈的freestyle,對于調(diào)用者來講,都屬于freestyle表演。

2、子類中可以增加自己特有的方法。

繼承一個(gè)很重要的特點(diǎn):子類繼承父類后可以新增方法。

/**
 * 跳舞
 * 子類中增加特有的方法
 */
public void dance(){
    System.out.println("跳鬼步!");
}

在Rapper中可以增加dance方法。

3、當(dāng)子類重載父類的方法時(shí),方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松。

注意,是子類重載父類,而不是子類重寫父類。

重載的話,相當(dāng)于一個(gè)全新的方法,與父類的同名方法并不沖突。兩個(gè)是同時(shí)存在的,根據(jù)傳入?yún)?shù)而自動(dòng)選擇方法。

可以重載抽象方法,也可以重載非抽象方法。

方法的形參為什么要比父類更寬松呢?

首先,形參肯定不能一致,一致的話,就是重寫了,就又回到第一條含義了。

第二,如果我們更加嚴(yán)格,那會(huì)出現(xiàn)什么情況呢?

我們可以來看下面的例子。

package com.fanqiekt.principle.liskov.rapper;

import java.util.List;

/**
 * 父類
 *
 * @author 番茄課堂-懶人
 */
public abstract class Parent {

    public void setList(List<String> list){
        System.out.println("執(zhí)行父類setList方法");
    }
}

這個(gè)是父類,setList方法有個(gè)List類型的形參。>

package com.fanqiekt.principle.liskov.rapper;

import java.util.ArrayList;

/**
 * 子類
 *
 * @author 番茄課堂-懶人
 */
public class Children extends Parent {

    public void setList(ArrayList<String> list) {
        System.out.println("執(zhí)行子類setList方法");
    }
}

這個(gè)是子類,傳入?yún)?shù)類型為ArrayList,比父類更加的嚴(yán)格。

Children children = new Children();
children.setList(new ArrayList<>());

我們運(yùn)行這行代碼,看下結(jié)果。

執(zhí)行子類setList方法

這個(gè)結(jié)果有沒有問題?

是有問題的,setList(new ArrayList<>())按照里氏替換原則是應(yīng)該透明的執(zhí)行父類的setList(List<String> list)方法的。

這塊不是很好理解,對于調(diào)用者來講,我想調(diào)用的Parent的setList(List<String> list)方法,結(jié)果卻執(zhí)行Children的setList(ArrayList<String> list)方法了。

這就好像是子類重寫了父類的setList方法,而不是重載了子類的setList方法。

也就是說,方法的形參嚴(yán)格后,在某種情況就變成重寫了。

而重寫顯然是不符合里氏替換原則的。

那我們再來看看寬松版本的。

/**
 * 子類
 *
 * @author 番茄課堂-懶人
 */
public class Children extends Parent {

    public void setList(Collection<String> list) {
        System.out.println("執(zhí)行子類setList方法");
    }
}

子類,傳入?yún)?shù)類型為Collection,比父類更加的寬松。

Children children = new Children();
children.setList(new ArrayList<>());

同樣的,我們運(yùn)行這行代碼,看下結(jié)果。

執(zhí)行父類setList方法
Children children = new Children();
children.setList(new HashSet<>());

同樣的,我們運(yùn)行這行代碼,看下結(jié)果。

執(zhí)行子類setList方法

傳入?yún)?shù)類型更加寬松,實(shí)現(xiàn)了子類重載父類。

4、當(dāng)子類的方法實(shí)現(xiàn)父類的抽象方法時(shí),方法的后置條件(即方法的返回值)要比父類更嚴(yán)格。

注意,這里說的是重寫抽象方法,非抽象方法是不能重寫的。

為什么說子類實(shí)現(xiàn)父類的抽象方法時(shí),返回值要更嚴(yán)格呢?

package com.fanqiekt.principle.liskov.rapper;

import java.util.List;

/**
 * 父類
 *
 * @author 番茄課堂-懶人
 */
public abstract class Parent {

    public abstract List<String> getList();
}

父類,有一個(gè)getList的抽象方法,返回值為List。

package com.fanqiekt.principle.liskov.rapper;

import java.util.List;

/**
 * 子類
 *
 * @author 番茄課堂-懶人
 */
public class Children extends Parent {

    @Override
    public Collection<String> getList() {
        return new ArrayList<>();
    }
}

子類,getList返回為Collection類型,類型更寬松。

會(huì)有紅線提示:... attempting to use incompatible return type 。

因?yàn)?,父類返回值是List,子類返回值是List的父類Collection,透明使用父類的時(shí)候則需要將Collection轉(zhuǎn)換成List。
類向上轉(zhuǎn)換是安全的,向下轉(zhuǎn)換則不一定是安全了。

package com.fanqiekt.principle.liskov.rapper;

import java.util.List;

/**
 * 子類
 *
 * @author 番茄課堂-懶人
 */
public class Children extends Parent {

    @Override
    public ArrayList<String> getList() {
        return new ArrayList<>();
    }
}

子類,getList返回為ArrayList類型,類型更嚴(yán)格。

將ArrayList轉(zhuǎn)換成List,向上轉(zhuǎn)換是安全的。

2、場景

八大菜系的廚師

番茄餐廳,經(jīng)過兢兢業(yè)業(yè)的經(jīng)營,從一家小型的餐館成長為一家大型餐廳。

廚師:老板,咱們現(xiàn)在家大業(yè)大客流量也大,雖然我精力充沛,但我也架不住這么多人的摧殘。

老板:摧殘?你確定?

廚師:哪能,您聽錯(cuò)了,是照顧,架不住這么多人的照顧。

老板:小火雞,可以呀,求生欲很強(qiáng)嘛。那你有什么想法?

廚師:我覺得咱們可以引入八大菜系廚師,一來,什么菜系的菜就交給什么菜系的廚師,味道質(zhì)量會(huì)更加的上乘,才能配的上我們這么高規(guī)格的餐廳。

老板:嗯,說的有點(diǎn)道理,繼續(xù)說。

廚師:二來,人手多了,還可以增加上菜的速度,三來......

老板:有道理,馬上招聘廚師,小火雞,恭喜你,升官了,你就是未來的廚師長。因?yàn)槟闱笊娴暮軓?qiáng)。

廚師長:謝謝老板。(內(nèi)心:我求生欲很強(qiáng)?哪里強(qiáng)了?放學(xué)你別走,我讓你嘗嘗我的厲害,給你做一桌子好菜)

求生欲果真很強(qiáng)。

3、實(shí)現(xiàn)

不廢話,擼代碼。

package com.fanqiekt.principle.liskov;

/**
 * 抽象廚師類
 *
 * @author 番茄課堂-懶人
 */
public abstract class Chef {
    /**
     * 做飯
     * @param dishName 餐名
     */
    public void cook(String dishName){
        System.out.println("開始烹飪:"+dishName);

        cooking(dishName);

        System.out.println(dishName + "出鍋");
    }

    /**
     * 開始做飯
     */
    protected abstract void cooking(String dishName);
}

抽象廚師類,公有cook方法,負(fù)責(zé)廚師做飯的一些相同邏輯,例如開始烹飪的準(zhǔn)備工作,以及出鍋。

具體做飯的細(xì)節(jié)則提供一個(gè)抽象方法cooking(正在做飯),具體菜系廚師需要重寫該方法。

package com.fanqiekt.principle.liskov;

/**
 * 山東廚師
 *
 * @author 番茄課堂-懶人
 */
public class ShanDongChef extends Chef{
    @Override
    protected void cooking(String dishName) {
        switch (dishName){
            case "西紅柿炒雞蛋":
                cookingTomato();
                break;
            default:
                throw new IllegalArgumentException("未知餐品");
        }
    }

    /**
     * 炒西紅柿雞蛋
     */
    private void cookingTomato() {
        System.out.println("先炒雞蛋");
        System.out.println("再炒西紅柿");
        System.out.println("...");
    }
}

魯菜廚師ShanDongChef繼承了廚師抽象類Chef,實(shí)現(xiàn)了抽象方法cooking。

package com.fanqiekt.principle.liskov;

/**
 * 四川廚師
 *
 * @author 番茄課堂-懶人
 */
public class SiChuanChef extends Chef{
    @Override
    protected void cooking(String dishName) {
        switch (dishName){
            case "酸辣土豆絲":
                cookingPotato();
                break;
            default:
                throw new IllegalArgumentException("未知餐品");
        }
    }

    /**
     * 炒酸辣土豆絲
     */
    private void cookingPotato() {
        System.out.println("先放蔥姜蒜");
        System.out.println("再放土豆絲");
        System.out.println("...");
    }
}

川菜廚師SiChuanChef繼承了廚師抽象類Chef,實(shí)現(xiàn)了抽象方法cooking。

package com.fanqiekt.principle.liskov;

/**
 * 服務(wù)員
 *
 * @author 番茄課堂-懶人
 */
public class Waiter {
    /**
     * 點(diǎn)餐
     * @param dishName 餐名
     */
    public void order(String dishName){
        System.out.println("客人點(diǎn)餐:" + dishName);

        Chef chef = new SiChuanChef();
        switch(dishName) {
            case "西紅柿炒雞蛋":
                chef = new ShanDongChef();
                break;
            case "酸辣土豆絲":      //取款
                chef = new SiChuanChef();
                break;
        }
        chef.cook(dishName);

        System.out.println(dishName + "上桌啦,請您品嘗!");
    }
}

服務(wù)員類Waiter有一個(gè)點(diǎn)餐order方法,根據(jù)不同的菜名去通知相應(yīng)菜系的廚師去做菜。

這里就用到了里氏替換原則,引用父類Chef可以透明地使用子類ShanDongChef或者SiChuanChef。

package com.fanqiekt.principle.liskov;

/**
 * 客人
 *
 * @author 番茄課堂-懶人
 */
public class Client {
    public static void main(String args[]){
        Waiter waiter = new Waiter();
        waiter.order("西紅柿炒雞蛋");
        System.out.println("---------------");
        waiter.order("酸辣土豆絲");
    }
}

我們運(yùn)行一下。

客人點(diǎn)餐:西紅柿炒雞蛋
開始烹飪:西紅柿炒雞蛋
先炒雞蛋
再炒西紅柿
...
西紅柿炒雞蛋出鍋
西紅柿炒雞蛋上桌啦,請您品嘗!
---------------
客人點(diǎn)餐:酸辣土豆絲
開始烹飪:酸辣土豆絲
先放蔥姜蒜
再放土豆絲
...
酸辣土豆絲出鍋
酸辣土豆絲上桌啦,請您品嘗!

4、優(yōu)點(diǎn)

擼過代碼后,我們發(fā)現(xiàn)替換原則的幾個(gè)優(yōu)點(diǎn)。

里氏替換原則的核心思想就是繼承,所以優(yōu)點(diǎn)就是繼承的優(yōu)點(diǎn)。

代碼重用
通過繼承父類,我們可以重用很多代碼,例如廚師烹飪前的準(zhǔn)備工作和出鍋。

減少創(chuàng)建類的成本,每個(gè)子類都擁有父類的屬性和方法。

易維護(hù)易擴(kuò)展
通過繼承,子類可以更容易擴(kuò)展功能。

也更容易維護(hù)了,公用方法都在父類中,特定的方法都在特定的子類中。

5、缺點(diǎn)

同上可知,它的缺點(diǎn)就是繼承的缺點(diǎn)。

破壞封裝
繼承是侵入性的,所以會(huì)讓子類與父類之間緊密耦合。

子類不能改變父類
可能造成子類代碼冗余、靈活性降低,因?yàn)樽宇悡碛懈割惖乃蟹椒ê蛯傩浴?/p>

6、嘻哈說

閑來無事聽聽曲,知識已填腦中去;學(xué)習(xí)復(fù)習(xí)新方式,頭戴耳機(jī)不小覷。
番茄課堂,學(xué)習(xí)也要酷。
接下來,請您欣賞懶人為里氏替換原則創(chuàng)作的歌曲。

試聽請點(diǎn)擊這里

嘻哈說:里氏替換原則

隔壁的說唱歌手可以在樂庫播放的beat freestyle歌曲
他們表演默契得體還充滿樂趣
非抽象重寫不是合理
抽象的重寫不需客氣
這是屬于他們哲理
繼承是里氏替換的核心想法
引用父類的地方透明使用子類會(huì)讓代碼更加強(qiáng)大
子類可以有自己特有方法
重載父類時(shí)形參更加的廣大
不然可能覆蓋父類方法
重寫抽象方法時(shí)返回值類型要往下
因?yàn)轭愊蛏限D(zhuǎn)換可以把心放下
八大菜系每個(gè)廚師都有自己拿手的
那些共有基本功也都掌握透徹
優(yōu)點(diǎn)是易擴(kuò)展易維護(hù)自動(dòng)繼承父類擁有的
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 如需下載源碼,請?jiān)L問https://github.com/fengchuanfang/Liskov_Substit...
    afa1332閱讀 654評論 0 0
  • 本文集網(wǎng)絡(luò)上文章及自己coding和理解的結(jié)果而來,是設(shè)計(jì)模式學(xué)習(xí)的開篇。 本文介紹設(shè)計(jì)模式的一些概念,分類,和設(shè)...
    月落蝶殤閱讀 621評論 0 0
  • 設(shè)計(jì)模式六大原則 設(shè)計(jì)模式六大原則(1):單一職責(zé)原則 定義:不要存在多于一個(gè)導(dǎo)致類變更的原因。通俗的說,即一個(gè)類...
    viva158閱讀 823評論 0 1
  • 漫長的暑假終于結(jié)束,新的一學(xué)期開始了。今天,冷寂的校園熱鬧起來了。實(shí)中全校報(bào)名,706班的孩子們又回到校園...
    三峽姑娘閱讀 170評論 0 1
  • 沒見到你 我早早見到了迎春花 是你 引出了萬花的節(jié)日 可我還看見滿目 凋零的春景 夏日 我早早的成了那只大鵬展翅 ...
    原郎閱讀 425評論 9 11

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