嘻哈說:設計模式之里氏替換原則

一、定義

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

全部引用基類(父類)的地方必須能透明地使用其子類的對象。  通俗的說,子類能夠擴展父類功能,但不能改變父類原有功能。java

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

Parent parent = new Child();
複製代碼

重點來了,那麼如何透明地使用呢?bash

咱們來思考個問題,子類能夠改變父類的原有功能嗎?app

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;
    }
}
複製代碼

這樣好很差?ide

確定是很差的,原本是加法卻修改爲了減法,這顯然是不符合認知的。學習

它違背了里氏替換原則,子類改變了父類原有功能後,當咱們在引用父類的地方使用其子類的時候,沒辦法透明使用add方法了。ui

父類中凡是已經實現好的方法,其實是在設定一系列的規範和契約,雖然它不強制要求全部的子類必須聽從這些規範,可是若是子類對這些非抽象方法任意修改,就會對整個繼承體系形成破壞。spa

因此,透明使用的關鍵就是,子類不能改變父類原有功能。code

二、含義

一、子類能夠實現父類的抽象方法,可是不能覆蓋父類的非抽象方法。

剛纔咱們已經說過,子類不能改變父類的原有功能,因此子類不能覆蓋父類的非抽象方法。

子類能夠實現父類的抽象方法,must be,抽象方法原本就是讓子類實現的。

package com.fanqiekt.principle.liskov.rapper;

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

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

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

    /**
     * 表演
     * 播放伴奏,並進行freeStyle
     */
    public void perform(){
        playBeat();
        freeStyle();
    }
}
複製代碼

BaseRapper是一個抽象類,它表明着Rapper的基類。

Rapper通常的表演方式是隨機播放一首伴奏而後進行free style。

freeStyle則各有各的不一樣,因此將它寫成了一個抽象方法,讓子類自由發揮。

playBeat流程大可能是同樣的,從樂庫中隨意播放伴奏,因此將它寫成了一個非抽象方法。

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

package com.fanqiekt.principle.liskov.rapper;

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

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

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

    }

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

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

覆蓋了父類的非抽象方法playBeat,並將邏輯更改成打開麥克風,明顯違背了里氏替換原則。 這顯然是很是錯誤的寫法, 緣由是父類行爲與子類行爲不一致,不能夠透明的使用父類了。 播放伴奏你卻給我打開麥克風,你肯定不是在逗我?

我嘗試着將playBeat進行下修改。

/**
 * 子類覆蓋父類非抽象方法
 * 子類方法中調用super方法
 */
@Override
protected void playBeat() {
    super.playBeat();
    System.out.println("關閉麥克風");
}
複製代碼

在子類方法中調用super方法,這樣修改是否能夠?

不能夠,緣由是打開麥克風跟播放伴奏沒有任何邏輯上的關係。

透明使用子類的時候,雖然伴奏也會正常的播放,但卻在調用者不知情的狀況下關閉了麥克風,而關閉麥克風又明顯與播放伴奏無關。 這就對於調用者沒法作到真正的透明瞭。

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

我嘗試着將perform進行下修改。

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

perform方法我這樣修改能夠嗎?

這個卻是能夠的,爲何一樣是子類調用super方法,爲何playBeat不能夠,perform就能夠呢?

perform是表演,跳舞是表演的一種補充,屬於表演範疇,調用者能夠透明地調用perform方法。

安靜的freestyle仍是手舞足蹈的freestyle,對於調用者來說,都屬於freestyle表演。

二、子類中能夠增長本身特有的方法。

繼承一個很重要的特色:子類繼承父類後能夠新增方法。

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

在Rapper中能夠增長dance方法。

三、當子類重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬鬆。

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

重載的話,至關於一個全新的方法,與父類的同名方法並不衝突。兩個是同時存在的,根據傳入參數而自動選擇方法。

能夠重載抽象方法,也能夠重載非抽象方法。

方法的形參爲何要比父類更寬鬆呢?

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

第二,若是咱們更加嚴格,那會出現什麼狀況呢?

咱們能夠來看下面的例子。

package com.fanqiekt.principle.liskov.rapper;

import java.util.List;

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

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

這個是父類,setList方法有個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("執行子類setList方法");
    }
}
複製代碼

這個是子類,傳入參數類型爲ArrayList,比父類更加的嚴格。

Children children = new Children();
children.setList(new ArrayList<>());
複製代碼

咱們運行這行代碼,看下結果。

執行子類setList方法
複製代碼

這個結果有沒有問題?

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

這塊不是很好理解,對於調用者來說,我想調用的Parent的setList(List list)方法,結果卻執行Children的setList(ArrayList list)方法了。

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

也就是說,方法的形參嚴格後,在某種狀況就變成重寫了。

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

那咱們再來看看寬鬆版本的。

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

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

子類,傳入參數類型爲Collection,比父類更加的寬鬆。

Children children = new Children();
children.setList(new ArrayList<>());
複製代碼

一樣的,咱們運行這行代碼,看下結果。

執行父類setList方法
複製代碼
Children children = new Children();
children.setList(new HashSet<>());
複製代碼

一樣的,咱們運行這行代碼,看下結果。

執行子類setList方法
複製代碼

傳入參數類型更加寬鬆,實現了子類重載父類。

四、當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。

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

爲何說子類實現父類的抽象方法時,返回值要更嚴格呢?

package com.fanqiekt.principle.liskov.rapper;

import java.util.List;

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

    public abstract List<String> getList();
}
複製代碼

父類,有一個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類型,類型更寬鬆。

會有紅線提示:... attempting to use incompatible return type 。

由於,父類返回值是List,子類返回值是List的父類Collection,透明使用父類的時候則須要將Collection轉換成List。 類向上轉換是安全的,向下轉換則不必定是安全了。

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類型,類型更嚴格。

將ArrayList轉換成List,向上轉換是安全的。

二、場景

八大菜系的廚師

番茄餐廳,通過兢兢業業的經營,從一家小型的餐館成長爲一家大型餐廳。

廚師:老闆,我們如今家大業大客流量也大,雖然我精力充沛,但我也架不住這麼多人的摧殘。

老闆:摧殘?你肯定?

廚師:哪能,您聽錯了,是照顧,架不住這麼多人的照顧。

老闆:小火雞,能夠呀,求生欲很強嘛。那你有什麼想法?

廚師:我以爲我們能夠引入八大菜系廚師,一來,什麼菜系的菜就交給什麼菜系的廚師,味道質量會更加的上乘,才能配的上咱們這麼高規格的餐廳。

老闆:嗯,說的有點道理,繼續說。

廚師:二來,人手多了,還能夠增長上菜的速度,三來......

老闆:有道理,立刻招聘廚師,小火雞,恭喜你,升官了,你就是將來的廚師長。由於你求生欲真的很強。

廚師長:謝謝老闆。(心裏:我求生欲很強?哪裏強了?放學你別走,我讓你嚐嚐個人厲害,給你作一桌子好菜)

求生欲果然很強。

三、實現

不廢話,擼代碼。

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方法,負責廚師作飯的一些相同邏輯,例如開始烹飪的準備工做,以及出鍋。

具體作飯的細節則提供一個抽象方法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,實現了抽象方法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,實現了抽象方法cooking。

package com.fanqiekt.principle.liskov;

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

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

        System.out.println(dishName + "上桌啦,請您品嚐!");
    }
}
複製代碼

服務員類Waiter有一個點餐order方法,根據不一樣的菜名去通知相應菜系的廚師去作菜。

這裏就用到了里氏替換原則,引用父類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("酸辣土豆絲");
    }
}
複製代碼

咱們運行一下。

客人點餐:西紅柿炒雞蛋
開始烹飪:西紅柿炒雞蛋
先炒雞蛋
再炒西紅柿
...
西紅柿炒雞蛋出鍋
西紅柿炒雞蛋上桌啦,請您品嚐!
---------------
客人點餐:酸辣土豆絲
開始烹飪:酸辣土豆絲
先放蔥薑蒜
再放土豆絲
...
酸辣土豆絲出鍋
酸辣土豆絲上桌啦,請您品嚐!
複製代碼

四、優勢

擼過代碼後,咱們發現替換原則的幾個優勢。

里氏替換原則的核心思想就是繼承,因此優勢就是繼承的優勢。

代碼重用 經過繼承父類,咱們能夠重用不少代碼,例如廚師烹飪前的準備工做和出鍋。

減小建立類的成本,每一個子類都擁有父類的屬性和方法。

易維護易擴展 經過繼承,子類能夠更容易擴展功能。

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

五、缺點

同上可知,它的缺點就是繼承的缺點。

破壞封裝 繼承是侵入性的,因此會讓子類與父類之間緊密耦合。

子類不能改變父類 可能形成子類代碼冗餘、靈活性下降,由於子類擁有父類的全部方法和屬性。

六、嘻哈說

接下來,請您欣賞里氏替換原則的原創歌曲

嘻哈說:里氏替換原則
做曲:懶人
做詞:懶人
Rapper:懶人

隔壁的說唱歌手能夠在樂庫播放的beat freestyle歌曲
他們表演默契得體還充滿樂趣
非抽象重寫不是合理
抽象的重寫不需客氣
這是屬於他們哲理
繼承是里氏替換的核心想法
引用父類的地方透明使用子類會讓代碼更增強大
子類能夠有本身特有方法
重載父類時形參更加的廣大
否則可能覆蓋父類方法
重寫抽象方法時返回值類型要往下
由於類向上轉換能夠把心放下
八大菜系每一個廚師都有本身拿手的
那些共有基本功也都掌握透徹
優勢是易擴展易維護自動繼承父類擁有的
複製代碼

試聽請點擊這裏

閒來無事聽聽曲,知識已填腦中去;

學習複習新方式,頭戴耳機不小覷。

番茄課堂,學習也要酷。

相關文章
相關標籤/搜索