肯定有不少人跟我剛看到這項原則的時候一樣墓造,對這個原則的名字充滿疑惑祠斧。其實原因就是這項原則最早是在1988年速缆,由麻省理工學院的一位姓里的女士(Barbara Liskov)提出來的规哪。
定義1:如果對每一個類型為 T1的對象 o1烁落,都有類型為 T2 的對象o2鸟蜡,使得以 T1定義的所有程序 P 在所有的對象 o1 都代換成 o2 時膜赃,程序 P 的行為沒有發(fā)生變化,那么類型 T2 是類型 T1 的子類型揉忘。
定義2:所有引用基類的地方必須能透明地使用其子類的對象跳座。
問題由來:有一功能P1,由類A完成∑現(xiàn)需要將功能P1進行擴展疲眷,擴展后的功能為P,其中P由原有功能P1與新功能P2組成乳蓄。新功能P由類A的子類B來完成咪橙,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發(fā)生故障虚倒。
解決方案:當使用繼承時美侦,遵循里氏替換原則。類B繼承類A時魂奥,除添加新的方法完成新增功能P2外菠剩,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法耻煤。
繼承包含這樣一層含義:父類中凡是已經(jīng)實現(xiàn)好的方法(相對于抽象方法而言)具壮,實際上是在設定一系列的規(guī)范和契約准颓,雖然它不強制要求所有的子類必須遵從這些契約,但是如果子類對這些非抽象方法任意修改棺妓,就會對整個繼承體系造成破壞攘已。而里氏替換原則就是表達了這一層含義。
繼承作為面向對象三大特性之一怜跑,在給程序設計帶來巨大便利的同時样勃,也帶來了弊端。比如使用繼承會給程序帶來侵入性性芬,程序的可移植性降低峡眶,增加了對象間的耦合性,如果一個類被其他的類所繼承植锉,則當這個類需要修改時辫樱,必須考慮到所有的子類,并且父類修改后俊庇,所有涉及到子類的功能都有可能會產生故障狮暑。
舉例說明繼承的風險,我們需要完成一個兩數(shù)相減的功能辉饱,由類ObjectSuper來負責心例。
運行結果:
后來,我們需要增加一個新的功能:完成兩數(shù)相加鞋囊,然后再與100求和,由類ObjectSubClass來負責瞎惫。即類B需要完成兩個功能:
兩數(shù)相減溜腐。
兩數(shù)相加,然后再加100瓜喇。
由于類ObjectClass已經(jīng)實現(xiàn)了第一個功能挺益,所以類ObjectSubClass繼承類ObjectSuper后,只需要再完成第二個功能就可以了乘寒,代碼如下:
類B完成后望众,運行結果:
我們發(fā)現(xiàn)原本運行正常的相減功能發(fā)生了錯誤。原因就是類B在給方法起名時無意中重寫了父類的方法伞辛,造成所有運行相減功能的代碼全部調用了類ObjectSubClass重寫后的方法烂翰,造成原本運行正常的功能出現(xiàn)了錯誤。在本例中蚤氏,引用基類ObjectSuper完成的功能甘耿,換成子類ObjectSubClass之后,發(fā)生了異常竿滨。在實際編程中佳恬,我們常常會通過重寫父類的方法來完成新的功能捏境,這樣寫起來雖然簡單,但是整個繼承體系的可復用性會比較差毁葱,特別是運用多態(tài)比較頻繁時垫言,程序運行出錯的幾率非常大。如果非要重寫父類的方法倾剿,比較通用的做法是:原來的父類和子類都繼承一個更通俗的基類筷频,原有的繼承關系去掉,采用依賴柱告、聚合截驮,組合等關系代替。
里氏替換原則通俗的來講就是:子類可以擴展父類的功能际度,但不能改變父類原有的功能葵袭。它包含以下4層含義:
子類可以實現(xiàn)父類的抽象方法,但不能覆蓋父類的非抽象方法乖菱。
子類中可以增加自己特有的方法坡锡。
當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松窒所。
當子類的方法實現(xiàn)父類的抽象方法時鹉勒,方法的后置條件(即方法的返回值)要比父類更嚴格。
看上去很不可思議吵取,因為我們會發(fā)現(xiàn)在自己編程中常常會違反里氏替換原則禽额,程序照樣跑的好好的。所以大家都會產生這樣的疑問皮官,假如我非要不遵循里氏替換原則會有什么后果脯倒?
后果就是:你寫的代碼出問題的幾率將會大大增加。