摘要:定義按照慣例,首先我們來(lái)看一下里氏替換原則的定義。同樣覆蓋了父類(lèi)的非抽象方法,并將邏輯更改為跳舞,這要是違背了里氏替換原則的。而重寫(xiě)顯然是不符合里氏替換原則的。里氏替換原則的核心思想就是繼承,所以優(yōu)點(diǎn)就是繼承的優(yōu)點(diǎn)。
1、定義
按照慣例,首先我們來(lái)看一下里氏替換原則的定義。
所有引用基類(lèi)(父類(lèi))的地方必須能透明地使用其子類(lèi)的對(duì)象。?
通俗的說(shuō),子類(lèi)可以擴(kuò)展父類(lèi)功能,但不能改變父類(lèi)原有功能。
核心思想是繼承。 通過(guò)繼承,引用基類(lèi)的地方就可以使用其子類(lèi)的對(duì)象了。例如:
Parent parent = new Child();
重點(diǎn)來(lái)了,那么如何透明地使用呢?
我們來(lái)思考個(gè)問(wèn)題,子類(lèi)可以改變父類(lèi)的原有功能嗎?
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; } }
這樣好不好?
肯定是不好的,本來(lái)是加法卻修改成了減法,這顯然是不符合認(rèn)知的。
它違背了里氏替換原則,子類(lèi)改變了父類(lèi)原有功能后,當(dāng)我們?cè)谝酶割?lèi)的地方使用其子類(lèi)的時(shí)候,沒(méi)辦法透明使用add方法了。
父類(lèi)中凡是已經(jīng)實(shí)現(xiàn)好的方法,實(shí)際上是在設(shè)定一系列的規(guī)范和契約,雖然它不強(qiáng)制要求所有的子類(lèi)必須遵從這些規(guī)范,但是如果子類(lèi)對(duì)這些非抽象方法任意修改,就會(huì)對(duì)整個(gè)繼承體系造成破壞。
所以,透明使用的關(guān)鍵就是,子類(lèi)不能改變父類(lèi)原有功能。
2、含義1、子類(lèi)可以實(shí)現(xiàn)父類(lèi)的抽象方法,但是不能覆蓋父類(lèi)的非抽象方法。
剛才我們已經(jīng)說(shuō)過(guò),子類(lèi)不能改變父類(lèi)的原有功能,所以子類(lèi)不能覆蓋父類(lèi)的非抽象方法。
子類(lèi)可以實(shí)現(xiàn)父類(lèi)的抽象方法,must be,抽象方法本來(lái)就是讓子類(lèi)實(shí)現(xiàn)的。
package com.fanqiekt.principle.liskov.rapper; /** * Rapper抽象類(lèi) * * @Author: 番茄課堂-懶人 */ public abstract class BaseRapper { /** * freeStyle */ protected abstract void freeStyle(); /** * 播放伴奏 */ protected void playBeat(){ System.out.println("從樂(lè)庫(kù)中隨機(jī)播放一首伴奏:動(dòng)次打次..."); } /** * 表演 * 播放伴奏,并進(jìn)行freeStyle */ public void perform(){ playBeat(); freeStyle(); } }
BaseRapper是一個(gè)抽象類(lèi),它代表著Rapper的基類(lèi)。
Rapper一般的表演方式是隨機(jī)播放一首伴奏然后進(jìn)行free style。
freeStyle則各有各的不同,所以將它寫(xiě)成了一個(gè)抽象方法,讓子類(lèi)自由發(fā)揮。
playBeat流程大多是一樣的,從樂(lè)庫(kù)中隨意播放伴奏,所以將它寫(xiě)成了一個(gè)非抽象方法。
perform的流程大多也是一樣的,放伴奏,然后freestyle,也將它寫(xiě)成了非抽象方法。
package com.fanqiekt.principle.liskov.rapper; /** * Rapper * * @author 番茄課堂-懶人 */ public class Rapper extends BaseRapper { /** * 播放伴奏 * * 子類(lèi)覆蓋父類(lèi)非抽象方法 */ @Override protected void playBeat() { System.out.println("關(guān)閉麥克風(fēng)"); } /** * 表演 * * 子類(lèi)覆蓋父類(lèi)非抽象方法 */ @Override public void perform() { System.out.println("跳鬼步"); } /** * 子類(lèi)可以覆蓋父類(lèi)抽象方法 */ @Override protected void freeStyle() { System.out.println("藥藥切克鬧,煎餅果子來(lái)一套!"); } }
Rapper是BaseRapper的子類(lèi),覆蓋了父類(lèi)的抽象方法freeStyle。
覆蓋了父類(lèi)的非抽象方法playBeat,并將邏輯更改為打開(kāi)麥克風(fēng),明顯違背了里氏替換原則。
這顯然是非常錯(cuò)誤的寫(xiě)法, 原因是父類(lèi)行為與子類(lèi)行為不一致,不可以透明的使用父類(lèi)了。
播放伴奏你卻給我打開(kāi)麥克風(fēng),你確定不是在逗我?
我嘗試著將playBeat進(jìn)行下修改。
/** * 子類(lèi)覆蓋父類(lèi)非抽象方法 * 子類(lèi)方法中調(diào)用super方法 */ @Override protected void playBeat() { super.playBeat(); System.out.println("關(guān)閉麥克風(fēng)"); }
在子類(lèi)方法中調(diào)用super方法,這樣修改是否可以?
不可以,原因是打開(kāi)麥克風(fēng)跟播放伴奏沒(méi)有任何邏輯上的關(guān)系。
透明使用子類(lèi)的時(shí)候,雖然伴奏也會(huì)正常的播放,但卻在調(diào)用者不知情的情況下關(guān)閉了麥克風(fēng),而關(guān)閉麥克風(fēng)又明顯與播放伴奏無(wú)關(guān)。
這就對(duì)于調(diào)用者無(wú)法做到真正的透明了。
同樣覆蓋了父類(lèi)的非抽象方法perform,并將邏輯更改為跳舞,這要是違背了里氏替換原則的。
只跳舞不說(shuō)唱的表演還叫Rapper嗎?
我嘗試著將perform進(jìn)行下修改。
/** * 表演 * freestyle + 跳舞 * 子類(lèi)覆蓋父類(lèi)非抽象方法 */ @Override public void perform() { super.perform(); System.out.println("跳鬼步"); }
perform方法我這樣修改可以嗎?
這個(gè)倒是可以的,為什么同樣是子類(lèi)調(diào)用super方法,為什么playBeat不可以,perform就可以呢?
perform是表演,跳舞是表演的一種補(bǔ)充,屬于表演范疇,調(diào)用者可以透明地調(diào)用perform方法。
安靜的freestyle還是手舞足蹈的freestyle,對(duì)于調(diào)用者來(lái)講,都屬于freestyle表演。
2、子類(lèi)中可以增加自己特有的方法。
繼承一個(gè)很重要的特點(diǎn):子類(lèi)繼承父類(lèi)后可以新增方法。
/** * 跳舞 * 子類(lèi)中增加特有的方法 */ public void dance(){ System.out.println("跳鬼步!"); }
在Rapper中可以增加dance方法。
3、當(dāng)子類(lèi)重載父類(lèi)的方法時(shí),方法的前置條件(即方法的形參)要比父類(lèi)方法的輸入?yún)?shù)更寬松。
注意,是子類(lèi)重載父類(lèi),而不是子類(lèi)重寫(xiě)父類(lèi)。
重載的話,相當(dāng)于一個(gè)全新的方法,與父類(lèi)的同名方法并不沖突。兩個(gè)是同時(shí)存在的,根據(jù)傳入?yún)?shù)而自動(dòng)選擇方法。
可以重載抽象方法,也可以重載非抽象方法。
方法的形參為什么要比父類(lèi)更寬松呢?
首先,形參肯定不能一致,一致的話,就是重寫(xiě)了,就又回到第一條含義了。
第二,如果我們更加嚴(yán)格,那會(huì)出現(xiàn)什么情況呢?
我們可以來(lái)看下面的例子。
package com.fanqiekt.principle.liskov.rapper; import java.util.List; /** * 父類(lèi) * * @author 番茄課堂-懶人 */ public abstract class Parent { public void setList(Listlist){ System.out.println("執(zhí)行父類(lèi)setList方法"); } }
這個(gè)是父類(lèi),setList方法有個(gè)List類(lèi)型的形參。>
package com.fanqiekt.principle.liskov.rapper; import java.util.ArrayList; /** * 子類(lèi) * * @author 番茄課堂-懶人 */ public class Children extends Parent { public void setList(ArrayListlist) { System.out.println("執(zhí)行子類(lèi)setList方法"); } }
這個(gè)是子類(lèi),傳入?yún)?shù)類(lèi)型為ArrayList,比父類(lèi)更加的嚴(yán)格。
Children children = new Children(); children.setList(new ArrayList<>());
我們運(yùn)行這行代碼,看下結(jié)果。
執(zhí)行子類(lèi)setList方法
這個(gè)結(jié)果有沒(méi)有問(wèn)題?
是有問(wèn)題的,setList(new ArrayList<>())按照里氏替換原則是應(yīng)該透明的執(zhí)行父類(lèi)的setList(List
這塊不是很好理解,對(duì)于調(diào)用者來(lái)講,我想調(diào)用的Parent的setList(List
這就好像是子類(lèi)重寫(xiě)了父類(lèi)的setList方法,而不是重載了子類(lèi)的setList方法。
也就是說(shuō),方法的形參嚴(yán)格后,在某種情況就變成重寫(xiě)了。
而重寫(xiě)顯然是不符合里氏替換原則的。
那我們?cè)賮?lái)看看寬松版本的。
/** * 子類(lèi) * * @author 番茄課堂-懶人 */ public class Children extends Parent { public void setList(Collectionlist) { System.out.println("執(zhí)行子類(lèi)setList方法"); } }
子類(lèi),傳入?yún)?shù)類(lèi)型為Collection,比父類(lèi)更加的寬松。
Children children = new Children(); children.setList(new ArrayList<>());
同樣的,我們運(yùn)行這行代碼,看下結(jié)果。
執(zhí)行父類(lèi)setList方法
Children children = new Children(); children.setList(new HashSet<>());
同樣的,我們運(yùn)行這行代碼,看下結(jié)果。
執(zhí)行子類(lèi)setList方法
傳入?yún)?shù)類(lèi)型更加寬松,實(shí)現(xiàn)了子類(lèi)重載父類(lèi)。
4、當(dāng)子類(lèi)的方法實(shí)現(xiàn)父類(lèi)的抽象方法時(shí),方法的后置條件(即方法的返回值)要比父類(lèi)更嚴(yán)格。
注意,這里說(shuō)的是重寫(xiě)抽象方法,非抽象方法是不能重寫(xiě)的。
為什么說(shuō)子類(lèi)實(shí)現(xiàn)父類(lèi)的抽象方法時(shí),返回值要更嚴(yán)格呢?
package com.fanqiekt.principle.liskov.rapper; import java.util.List; /** * 父類(lèi) * * @author 番茄課堂-懶人 */ public abstract class Parent { public abstract ListgetList(); }
父類(lèi),有一個(gè)getList的抽象方法,返回值為L(zhǎng)ist。
package com.fanqiekt.principle.liskov.rapper; import java.util.List; /** * 子類(lèi) * * @author 番茄課堂-懶人 */ public class Children extends Parent { @Override public CollectiongetList() { return new ArrayList<>(); } }
子類(lèi),getList返回為Collection類(lèi)型,類(lèi)型更寬松。
會(huì)有紅線提示:... attempting to use incompatible return type 。
因?yàn)?,父?lèi)返回值是List,子類(lèi)返回值是List的父類(lèi)Collection,透明使用父類(lèi)的時(shí)候則需要將Collection轉(zhuǎn)換成List。
類(lèi)向上轉(zhuǎn)換是安全的,向下轉(zhuǎn)換則不一定是安全了。
package com.fanqiekt.principle.liskov.rapper; import java.util.List; /** * 子類(lèi) * * @author 番茄課堂-懶人 */ public class Children extends Parent { @Override public ArrayListgetList() { return new ArrayList<>(); } }
子類(lèi),getList返回為ArrayList類(lèi)型,類(lèi)型更嚴(yán)格。
將ArrayList轉(zhuǎn)換成List,向上轉(zhuǎn)換是安全的。
2、場(chǎng)景八大菜系的廚師
番茄餐廳,經(jīng)過(guò)兢兢業(yè)業(yè)的經(jīng)營(yíng),從一家小型的餐館成長(zhǎng)為一家大型餐廳。
廚師:老板,咱們現(xiàn)在家大業(yè)大客流量也大,雖然我精力充沛,但我也架不住這么多人的摧殘。
老板:摧殘?你確定?
廚師:哪能,您聽(tīng)錯(cuò)了,是照顧,架不住這么多人的照顧。
老板:小火雞,可以呀,求生欲很強(qiáng)嘛。那你有什么想法?
廚師:我覺(jué)得咱們可以引入八大菜系廚師,一來(lái),什么菜系的菜就交給什么菜系的廚師,味道質(zhì)量會(huì)更加的上乘,才能配的上我們這么高規(guī)格的餐廳。
老板:嗯,說(shuō)的有點(diǎn)道理,繼續(xù)說(shuō)。
廚師:二來(lái),人手多了,還可以增加上菜的速度,三來(lái)......
老板:有道理,馬上招聘廚師,小火雞,恭喜你,升官了,你就是未來(lái)的廚師長(zhǎng)。因?yàn)槟闱笊娴暮軓?qiáng)。
廚師長(zhǎng):謝謝老板。(內(nèi)心:我求生欲很強(qiáng)?哪里強(qiáng)了?放學(xué)你別走,我讓你嘗嘗我的厲害,給你做一桌子好菜)
求生欲果真很強(qiáng)。
3、實(shí)現(xiàn)不廢話,擼代碼。
package com.fanqiekt.principle.liskov; /** * 抽象廚師類(lèi) * * @author 番茄課堂-懶人 */ public abstract class Chef { /** * 做飯 * @param dishName 餐名 */ public void cook(String dishName){ System.out.println("開(kāi)始烹飪:"+dishName); cooking(dishName); System.out.println(dishName + "出鍋"); } /** * 開(kāi)始做飯 */ protected abstract void cooking(String dishName); }
抽象廚師類(lèi),公有cook方法,負(fù)責(zé)廚師做飯的一些相同邏輯,例如開(kāi)始烹飪的準(zhǔn)備工作,以及出鍋。
具體做飯的細(xì)節(jié)則提供一個(gè)抽象方法cooking(正在做飯),具體菜系廚師需要重寫(xiě)該方法。
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繼承了廚師抽象類(lèi)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繼承了廚師抽象類(lèi)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 + "上桌啦,請(qǐng)您品嘗!"); } }
服務(wù)員類(lèi)Waiter有一個(gè)點(diǎn)餐order方法,根據(jù)不同的菜名去通知相應(yīng)菜系的廚師去做菜。
這里就用到了里氏替換原則,引用父類(lèi)Chef可以透明地使用子類(lèi)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)餐:西紅柿炒雞蛋 開(kāi)始烹飪:西紅柿炒雞蛋 先炒雞蛋 再炒西紅柿 ... 西紅柿炒雞蛋出鍋 西紅柿炒雞蛋上桌啦,請(qǐng)您品嘗! --------------- 客人點(diǎn)餐:酸辣土豆絲 開(kāi)始烹飪:酸辣土豆絲 先放蔥姜蒜 再放土豆絲 ... 酸辣土豆絲出鍋 酸辣土豆絲上桌啦,請(qǐng)您品嘗!4、優(yōu)點(diǎn)
擼過(guò)代碼后,我們發(fā)現(xiàn)替換原則的幾個(gè)優(yōu)點(diǎn)。
里氏替換原則的核心思想就是繼承,所以優(yōu)點(diǎn)就是繼承的優(yōu)點(diǎn)。
代碼重用
通過(guò)繼承父類(lèi),我們可以重用很多代碼,例如廚師烹飪前的準(zhǔn)備工作和出鍋。
減少創(chuàng)建類(lèi)的成本,每個(gè)子類(lèi)都擁有父類(lèi)的屬性和方法。
易維護(hù)易擴(kuò)展
通過(guò)繼承,子類(lèi)可以更容易擴(kuò)展功能。
也更容易維護(hù)了,公用方法都在父類(lèi)中,特定的方法都在特定的子類(lèi)中。
5、缺點(diǎn)同上可知,它的缺點(diǎn)就是繼承的缺點(diǎn)。
破壞封裝
繼承是侵入性的,所以會(huì)讓子類(lèi)與父類(lèi)之間緊密耦合。
子類(lèi)不能改變父類(lèi)
可能造成子類(lèi)代碼冗余、靈活性降低,因?yàn)樽宇?lèi)擁有父類(lèi)的所有方法和屬性。
接下來(lái),請(qǐng)您欣賞里氏替換原則的原創(chuàng)歌曲。
嘻哈說(shuō):里氏替換原則 作曲:懶人 作詞:懶人 Rapper:懶人 隔壁的說(shuō)唱歌手可以在樂(lè)庫(kù)播放的beat freestyle歌曲 他們表演默契得體還充滿樂(lè)趣 非抽象重寫(xiě)不是合理 抽象的重寫(xiě)不需客氣 這是屬于他們哲理 繼承是里氏替換的核心想法 引用父類(lèi)的地方透明使用子類(lèi)會(huì)讓代碼更加強(qiáng)大 子類(lèi)可以有自己特有方法 重載父類(lèi)時(shí)形參更加的廣大 不然可能覆蓋父類(lèi)方法 重寫(xiě)抽象方法時(shí)返回值類(lèi)型要往下 因?yàn)轭?lèi)向上轉(zhuǎn)換可以把心放下 八大菜系每個(gè)廚師都有自己拿手的 那些共有基本功也都掌握透徹 優(yōu)點(diǎn)是易擴(kuò)展易維護(hù)自動(dòng)繼承父類(lèi)擁有的
試聽(tīng)請(qǐng)點(diǎn)擊這里
閑來(lái)無(wú)事聽(tīng)聽(tīng)曲,知識(shí)已填腦中去;
學(xué)習(xí)復(fù)習(xí)新方式,頭戴耳機(jī)不小覷。
番茄課堂,學(xué)習(xí)也要酷。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/77257.html
摘要:前言本章我們要講解的是五大原則語(yǔ)言實(shí)現(xiàn)的第篇,里氏替換原則。因此,違反了里氏替換原則。與行為有關(guān),而不是繼承到現(xiàn)在,我們討論了和繼承上下文在內(nèi)的里氏替換原則,指示出的面向?qū)ο蟆? 前言 本章我們要講解的是S.O.L.I.D五大原則JavaScript語(yǔ)言實(shí)現(xiàn)的第3篇,里氏替換原則LSP(The Liskov Substitution Principle )。英文原文:http://fre...
摘要:在面向?qū)ο笤O(shè)計(jì)中,可維護(hù)性的復(fù)用是以設(shè)計(jì)原則為基礎(chǔ)的。面向?qū)ο笤O(shè)計(jì)原則為支持可維護(hù)性復(fù)用而誕生,這些原則蘊(yùn)含在很多設(shè)計(jì)模式中,它們是從許多設(shè)計(jì)方案中總結(jié)出的指導(dǎo)性原則。 面向?qū)ο笤O(shè)計(jì)原則 概述 對(duì)于面向?qū)ο筌浖到y(tǒng)的設(shè)計(jì)而言,在支持可維護(hù)性的同時(shí),提高系統(tǒng)的可復(fù)用性是一個(gè)至關(guān)重要的問(wèn)題,如何同時(shí)提高一個(gè)軟件系統(tǒng)的可維護(hù)性和可復(fù)用性是面向?qū)ο笤O(shè)計(jì)需要解決的核心問(wèn)題之一。在面向?qū)ο笤O(shè)計(jì)中,...
摘要:?jiǎn)我宦氊?zé)原則開(kāi)閉原則里氏替換原則依賴倒置原則接口隔離原則迪米特法則組合聚合復(fù)用原則單一職責(zé)原則高內(nèi)聚低耦合定義不要存在多于一個(gè)導(dǎo)致類(lèi)變更的原因。建議接口一定要做到單一職責(zé),類(lèi)的設(shè)計(jì)盡量做到只有一個(gè)原因引起變化。使用繼承時(shí)遵循里氏替換原則。 單一職責(zé)原則 開(kāi)閉原則 里氏替換原則 依賴倒置原則 接口隔離原則 迪米特法則 組合/聚合復(fù)用原則 單一職責(zé)原則(Single Responsi...
閱讀 2175·2021-11-11 16:55
閱讀 1697·2019-08-30 15:54
閱讀 2827·2019-08-30 15:53
閱讀 2224·2019-08-30 15:44
閱讀 1159·2019-08-30 15:43
閱讀 974·2019-08-30 11:22
閱讀 1954·2019-08-29 17:20
閱讀 1576·2019-08-29 16:56