java設計原則--里氏替換原則
JAVA設計原則–里氏替換原則(LSP原則)
為什麼要用里氏替換原則?:
為了優化繼承所帶來的缺點,使得繼承的優點發揮到最大,而同時減少缺點帶來的麻煩。
繼承的優缺點:
優點:
1. 程式碼共享,減少建立類的工作量,每個子類都擁有父類的屬性和方法
2. 提高程式碼的重用性(子類可以使用父類的屬性和方法)
3. 子類可以在父類的基礎上進行拓展(重寫父類的方法,實現自己的邏輯)(很多開源框架的擴充套件介面都是通過繼承父類來實現的)。
缺點:
1. 繼承是具有侵入性的。也就是隻要繼承,就必須擁有父類的所有屬性和方法
2. 降低了子類的靈活性。同強增加了耦合性。(因為子類具有父類的屬性和變數,所以,在修改父類的屬性和方法時,需要考慮子類的修改,而這種修改如果沒有規範,可能需要大段的程式碼重構)
里氏替換原則的規範(定義):
1、所有引用父類的地方都必須能透明地使用其子類的物件。
(只要哪裡使用了父類,那麼他的所有子類也必須能使用,替換子類不會發生任何錯誤或異常,呼叫者不需要知道是子類還是父類。)
2、子類出現的地方,父類不一定能適用。
里氏替換原則規範的含義:
1.子類必須完全實現父類的方法
例: 在平常編寫程式碼時,定義介面,然後編寫實現類,而在呼叫時,直接呼叫介面方法(高層抽象)(其他場景下還有抽象類這種情況),而不去呼叫具體的實現了,其實這裡已經使用了里氏替換原則。
例:士兵射擊的場景—–槍(本篇中的例子均來自<<設計模式之禪>>)
槍有很多種類,具體士兵使用什麼槍,得等到呼叫的時候才知道,所以需要我們對槍進行抽象:
槍支的抽象類:
public abstract class AbstractGun {
//定義模板射擊方法 具體讓子類去實現
public abstract void shoot();
// 槍的形狀
public void shape() { // 槍的形狀};
// 槍的聲音
public void voice( // 槍的聲音);
}
// 手槍
public class Handgun extends AbstractGun {
// 手槍的射擊方法
@Override
public void shoot() {
System.out.println("手槍射擊");
}
}
// 步槍
public class Rifle extends AbstractGun {
// 步槍的射擊
@Override
public void shoot() {
System.out.println("步槍射擊");
}
}
// 機槍
public class MachineGun extends AbstractGun {
// 機槍
@Override
public void shoot() {
System.out.println("機槍掃射");
}
}
有了槍支,還需要士兵去使用(呼叫)槍支:
// 士兵
public class Soldier {
// 定義士兵的槍支
private AbstractGun gun;
// 給士兵槍
public void setGun(AbstractGun _gun) {
this.gun = _gun;
}
// 射擊敵人
public void killEnemy() {
System.out.println("士兵開始殺敵人");
gun.shoot();
}
}
注意: 在Soldier類中,呼叫槍類時,呼叫了頂級父類AbstractGun ,這符合了里氏替換原則(LSP):
在類中呼叫其他類時,務必使用父類或介面(高層抽象),如果不能使用父類或介面,則說明類的設計已經違背了原則。
士兵有了,槍支有了,之後就需要在實際中(具體場景)去用具體的槍射擊敵人:
public class client {
public static void main(String[] args) {
// 產生士兵
Soldier soldier = new Soldier();
// 給士兵槍支 這裡給了步槍 如果要使用其他的槍,傳入其他具體的子類即可
soldier.setGun(new Rifile);
// 士兵射擊敵人
soldier.killEnemy();
}
}
注意:當出現特殊的子類,並且無法應用在父類的場景下時,應當對子類進行抽象,建立一個獨立的父類,並將槍支的抽象類與特殊的子類的抽象類建立委託關係。
// 因為玩具槍不滿足場景(不能殺敵人),所以得獨立建立父類,然後兩個父類下的子類各自延展
public abstract class AbstractToy {
// 與槍支的抽象類建立委託關係 將聲音、形狀等等一些特性都委託給AbstractGun處理
private AbstractGun gun;
// 委託給AbstractGun處理
public void shape() {
System.out.println("槍的形狀");
gun.shape();
};
// 委託給AbstractGun處理
public void voice() {
System.out.println("槍射擊的聲音");
gun.voice();
}
}
public class ToyGun extends AbstractToy {
// 玩具槍形狀
super.shape();
// 玩具槍聲音
super.shape();
}
注意: 如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關係,採用依賴、聚集、組合等關係替代繼承。
2.子類可以存在屬於自己的屬性和方法
里氏替換原則可以正著用,但是不能反著用(在子類出現的地方,父類未必能使用。)
public class AUG extends Rifile {
// 狙擊槍帶望遠鏡
public void zoomOut() {
System.out.println("通過望遠鏡觀察敵人");
}
//
public void shoot() {
System.out.println("AUG射擊。。。。。。");
}
}
// AUG狙擊手
public class Snipper {
public void killEnemy(Aug aug) {
// 觀察敵人
aug.zoomOut();
// 開始射擊
anu.shoot();
}
}
// 客戶端呼叫(使用子類的場景)
public class Client {
public static void main(String[] args) {
Snipper snipper = new Snipper();
snipper.setRile(new AUG());
snipper.killEnemy();
}
}
// 客戶端呼叫(使用父類替代子類)
public class Client {
public static void main(String[] args) {
Snipper snipper = new Snipper();
snipper.setRile((AUG) new Rifle());
snipper.killEnemy();
}
}
編寫上面那段程式碼,會發現在執行期間會丟擲java.lang.ClassCastException異常,也就是說,向下轉型是不安全的,從里氏替換這個原則上來看,就是子類出現的地方,父類不一定能使用。
3.重寫或者實現父類的方法時,輸入引數可以被放大
例:
// 返回Collection集合型別
public class Father {
public Collection doSomething(HashMap map) {
System.out.println("父類被執行...");
return map.values();
}
}
// 子類返回Collection集合型別
public class Son extends Father {
// 放大輸入引數型別
public Collection doSomething(Map map) {
System.out.println("子類被執行");
return map.values();
}
}
// 場景類
public class Client {
public static void invoker() {
// 父類存在的地方,子類可以替代
Father father = new Father();
HashMap map = new HashMap();
father.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
// 場景類
public class Client {
public static void invoker() {
Son son = new Son();
HashMap map = new HashMap();
son.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
// 然後會發現,上面兩種場景下執行結果相同。
注意: 在上面例子中,子類和父類的方法名相同,但是引數列表不同(方法引數型別不同),所以,這不是重寫,而是過載,因為繼承是讓子類擁有父類的屬性和方法,所以在子類中,有兩個方法,方法名相同,但是引數列表不同,所以是過載。
父類的引數型別是HashMap,子類的引數型別是Map,說明引數型別的範圍被擴大了,當傳入的引數是HashMap時,會發現子類代替父類執行(因為繼承了父類的方法),而真正的子類方法不會被呼叫。而子類想執行方法,必須重寫或過載父類的方法,這樣做是正確的。
因為如果父類引數的類型範圍大於子類的話,那麼父類出現的地方,子類未必可以使用,可能導致程式出錯。
總結: 子類的引數的範圍型別必須大於等於父類的引數的範圍型別
4.重寫或者實現父類的方法時輸出結果可以被縮小
父類的一個方法的返回值的型別是T,子類的相同方法(過載或重寫)的返回型別是S,那麼按照里氏替換原則,S必須小於等於T,也就是說,要麼S和T是同一個型別,要麼S是T的子類 :
第一種情況: 重寫:
父類和子類的同名方法的輸入引數是相同的,放個方法的範圍值S小於等於T,這個是重寫的要求(這是為了向上轉型;既然子類重寫了父類的方法,有時候就需要用父類物件引用來呼叫子類重寫的方法)
第二種情況:過載
要求方法的輸入引數型別或數量不相同,根據里氏替換原則,就是子類的引數範圍要大於或等於父類的引數範圍,也就是說,你寫的方法是不會被呼叫的。