定義:
程序中的對象應該是可以在不改變程序正確性的前提下被它的子類所替換,也就是說所有引用基類的地方必須能透明地使用其子類的對象。通俗的來說,子類可以擴展父類的功能,但不能改變父類原有的功能。
由來:
第一次看見這個裡氏替換原則的名字會覺著很奇特,根據以往的經驗這一看就是外國友人首先提出的概念,然後便以她的姓命名該原則。確實是這樣,它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次會議上名為“數據的抽象與層次”的演說中首先提出。裡氏替換原則英文名稱為Liskov Substitution principle,所以簡稱為LSP。
深入:
裡氏替換原則為良好的繼承定義了一個規范,一句簡單的定義包含了四層含義:
我們在做系統設計時經常編寫接口和抽象類,然後編碼繼承它們,其實這裡已經應用了裡氏替換原則。比如,我們簡單實現一下CS中的各種槍(定義抽象類,然後繼承),槍的類圖如下所示:
/// <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(); } }
運行結果如下所示:
注意在類中調用其他類時務必要使用父類或接口,如果不能使用父類或接口,則說明類的設計已經違背了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(); } }
運行結果如下:
關聯委托關系
注意:如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關系,采用依賴、聚集、組合等關系代替繼承。
子類當然可以有自己的行為和外觀(也就是方法和屬性),這裡為什麼要提呢?因為裡氏替換原則是不能到這使用的的,子類可以替換父類,但是父類不能替換父類(如果能替換也就沒必要派生子類了),具體就不解釋了(定義就是這麼定義的,道理比較淺顯),用下面的圖做一下簡單的說明:
子類中方法的前置條件必須與超類中被覆蓋的方法的前置條件相同或者更寬松。
這是什麼意思呢,父類的一個方法的返回值是一個類型T,子類的相同方法(重載或覆寫)的返回值為S,那麼裡氏替換原則就要求S必須小於等於T,也就是說要麼S和T是同一個類型,要麼S是T的子類,為什麼呢?分兩種情況,如果是覆寫,父類和子類的同名方法的輸入參數是相同的,兩個方法的范圍值S小於等於T,這是覆寫的要求,這才是重中之重,子類覆寫父類的方法,天經地義。如果是重載,則要求方法的輸入參數類型或數量不相同,在裡氏替換原則要求下,就是子類的輸入參數大於或等於父類的輸入參數,也就是說你寫的這個方法是不會被調用到的,參考上面講的前置條件。
采用裡氏替換原則的目的就是增強程序的健壯性,版本升級時也可以保持非常好的兼容性。即使增加子類,原有的子類還可以繼續運行。在實際項目中,每個子類對應不同的業務含義,使用父類作為參數,傳遞不同的子類完成不同的業務邏輯,非常完美!
總結:
看上去很不可思議,因為我們會發現在自己編程中常常會違反裡氏替換原則,程序照樣跑的好好的。所以大家都會產生這樣的疑問,假如我非要不遵循裡氏替換原則會有什麼後果?
後果就是:你寫的代碼出問題的幾率將會大大增加。
PS:最後提交時竟然提示[手.槍]是違禁詞,只好改成了shou槍
在面向對象思想中可知,派生類擁有基類向下公開的所有特征,它是基類的一個特例。
當派生類對象賦於基類類型時,將出現以下情況:派生類的數據結構依次對應於基類的數據結構。而派生類擁有的自己的數據將不可見。
當基類的對象試圖轉換為派生類型時,將出現基類對象的數據無法依次填充完派生類的所有數據結構。這就造成了它將無法完成派生類定義的功能。編譯器將會提示甚至報錯。
這就是派生類能勝任基類功能,而基類卻無法完全勝任派生類功能的原因。
強制轉換屬於 基類到派生的過程:那是因為 設計人員知道:該基類對象的數據結構完全可以填充完派生類的結構。否則,將出現強轉錯誤。一般最好避免使用強轉!
還有,子類能夠出現在任何父類對象出現的地方不是完全正確的,父類有時也不會將自己的一些成員公開給子類。
參考資料:無
子類可以替換父類了
也就是是父類出現的地方都可以用子類替代
switch不存在這個概念吧