定義:
程序中的對象應該是可以在不改變程序正確性的前提下被它的子類所替換,也就是說所有引用基類的地方必須能透明地使用其子類的對象。通俗的來說,子類可以擴展父類的功能,但不能改變父類原有的功能。
由來:
第一次看見這個里氏替換原則的名字會覺著很奇特,根據以往的經驗這一看就是外國友人首先提出的概念,然后便以她的姓命名該原則。確實是這樣,它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次會議上名為“數據的抽象與層次”的演說中首先提出。里氏替換原則英文名稱為Liskov Substitution PRinciple,所以簡稱為LSP。
深入:
里氏替換原則為良好的繼承定義了一個規范,一句簡單的定義包含了四層含義:
我們在做系統設計時經常編寫接口和抽象類,然后編碼繼承它們,其實這里已經應用了里氏替換原則。比如,我們簡單實現一下CS中的各種槍(定義抽象類,然后繼承),槍的類圖如下所示:
槍的主要職責是殺人,每種槍都有自己的特點。shou槍是單發且射程較近,步槍射程遠威力大,機槍主要用于掃射。同時定義一個士兵類,KillEnemy(AbstractGun gun)方法使用槍來殺人,具體使用什么槍調用時才會知道。
槍抽象類、常用槍和士兵類代碼實現如下所示:
/// <summary> /// 槍抽象類 /// </summary> public abstract class AbstractGun { public abstract void Shoot(); } /// <summary> /// shou槍 /// </summary> public class HandGun : AbstractGun { public override void Shoot() { Console.WriteLine("shou槍射擊..."); } } /// <summary> /// 步槍 /// </summary> public class Rifle : AbstractGun { public override void Shoot() { Console.WriteLine("步槍射擊..."); } } /// <summary> /// 機槍 /// </summary> public class MachineGun : AbstractGun { public override void Shoot() { Console.WriteLine("機槍射擊..."); } } /// <summary> /// 士兵類 /// </summary> public class Solder { public void KillEnemy(AbstractGun gun) { Console.WriteLine("士兵開始殺人..."); gun.Shoot(); } }
場景類(主函數)代碼如下所示:
class Client { static void Main(string[] args) { var solder = new Solder(); solder.KillEnemy(new Rifle()); Console.ReadKey(); } }
運行結果如下所示:
在這個程序中,我們給三毛這個士兵一把步槍,然后就開始殺敵了,如果三毛要使用機槍當然也可以,直接把solder.killEnemy(new Rifle())修改為 solder.killEnemy(new MachineGun())就可以了,在編寫程序時Solider士兵類根本就不用知道是哪個型號的槍(子類)被傳入。
注意在類中調用其他類時務必要使用父類或接口,如果不能使用父類或接口,則說明類的設計已經違背了LSP原則。
如果現在我們想嫁個玩具槍,那我們就加一個TonyGun類繼承AbstractGun,加入玩具槍后新的類圖如下所示:
但是考慮到實際情況,因為玩具槍一般是殺不死人的,所以玩具槍里的類是不能實現殺人的,如下代碼所示只能把該方法置空:
/// <summary> /// 玩具槍 /// </summary> public class TonyGun : AbstractGun { public override void Shoot() { //玩具槍不能射擊,這里就不能實現了 } }
把槍改為玩具槍,演示代碼如下所示:
class Client { static void Main(string[] args) { var solder = new Solder(); solder.KillEnemy(new TonyGun()); Console.ReadKey(); } }
運行結果如下:
結果只提示士兵殺人卻沒有發射子彈(因為用的是玩具槍,有點搞)。
在這種情況下,我們發現業務調用類已經出現了問題,正常的業務邏輯已經不能運行,那怎么辦?好辦,有兩種解決辦法:
1.在Soldier類中增加instanceof的判斷,如果是玩具槍,就不用來殺敵人。這個方法可以解決問題,但是你要知道,在程序中,每增加一個類,所有與這個父類有關系的類都必須修改,你覺得可行嗎?如果你的產品出現了這個問題,因為修正了這樣一個Bug,就要求所有與這個父類有關系的類都增加一個判斷,客戶非跳起來跟你干架不可!你還想要你的客戶忠誠你嗎?顯然,這個方案被否定了。
2.ToyGun脫離繼承,建立一個獨立的父類,為了做到代碼可以復用,可以與AbastractGun建立關聯委托關系
注意:如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關系,采用依賴、聚集、組合等關系代替繼承。
子類當然可以有自己的行為和外觀(也就是方法和屬性),這里為什么要提呢?因為里氏替換原則是不能到這使用的的,子類可以替換父類,但是父類不能替換父類(如果能替換也就沒必要派生子類了),具體就不解釋了(定義就是這么定義的,道理比較淺顯),用下面的圖做一下簡單的說明:
方法中的輸入參數稱為前置條件,這是什么意思呢?大家做過Web Service開發就應該知道有一個“契約優先”的原則,也就是先定義出WSDL接口,制定好雙方的開發協議,然后再各自實現。里氏替換原則也要求制定一個契約,就是父類或接口,這種設計方法也叫做Design by Contract,契約設計,是與里氏替換原則融合在一起的。契約制定了,也就同時制定了前置條件和后置條件,前置條件就是你要讓我執行,就必須滿足我的條件;后置條件就是我執行完了需要反饋,標準是什么。
子類在沒有覆寫父類的方法的前提下,子類方法被執行了,這會引起業務邏輯混亂,因為在實際應用中父類一般都是抽象類,子類是實現類,你傳遞一個這樣的實現類就會“歪曲”了父類的意圖,引起一堆意想不到的業務邏輯混亂,所以子類中方法的前置條件必須與超類中被覆蓋的方法的前置條件相同或者更寬松。
這是什么意思呢,父類的一個方法的返回值是一個類型T,子類的相同方法(重載或覆寫)的返回值為S,那么里氏替換原則就要求S必須小于等于T,也就是說要么S和T是同一個類型,要么S是T的子類,為什么呢?分兩種情況,如果是覆寫,父類和子類的同名方法的輸入參數是相同的,兩個方法的范圍值S小于等于T,這是覆寫的要求,這才是重中之重,子類覆寫父類的方法,天經地義。如果是重載,則要求方法的輸入參數類型或數量不相同,在里氏替換原則要求下,就是子類的輸入參數大于或等于父類的輸入參數,也就是說你寫的這個方法是不會被調用到的,參考上面講的前置條件。采用里氏替換原則的目的就是增強程序的健壯性,版本升級時也可以保持非常好的兼容性。即使增加子類,原有的子類還可以繼續運行。在實際項目中,每個子類對應不同的業務含義,使用父類作為參數,傳遞不同的子類完成不同的業務邏輯,非常完美!
總結:
看上去很不可思議,因為我們會發現在自己編程中常常會違反里氏替換原則,程序照樣跑的好好的。所以大家都會產生這樣的疑問,假如我非要不遵循里氏替換原則會有什么后果? 后果就是:你寫的代碼出問題的幾率將會大大增加。
PS:最后提交時竟然提示[手.槍]是違禁詞,只好改成了shou槍
新聞熱點
疑難解答