里氏代換原則——及之我見
第7章 里氏代換原則(LSP)
里氏代換原則 我感覺就是一句話,簡單的說就是 子型別必須能夠替換他們的父型別。就這麼簡單
面向物件設計的重要原則是建立抽象化,並且從抽象化匯出具體化。具體化可以給出不同的版本,每個版本都給出不同的實現。
從抽象化到具體化的匯出要是用繼承關係和里氏代換原則。(Liskov Substitution Principle).(LY注:實現OCP原則(開閉原則)的關鍵步驟是抽象化,而繼承是實現抽象方法的重要手段。里氏原則是對開閉原則的抽象化的具體步驟的補充。)
里氏代換原則由Barbara Liskov提出。
(LY注:Barbara Liskov,就職於麻省理工學院(MIT)電腦科學實驗室。里氏代換原則出自她1988年發表的經典文章Data Abstraction and Hierarchy,含義大致如下:使用指向基類的指標或引用的函式,必須能夠在不知道具體派生類物件型別的情況下使用它們.(FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT.)。原文見後,這裡需要如下的替換性質:若對於每一個型別S的物件o1,都存在一個型別T的物件o2,使得在所有針對T編寫的程式P中,用o1替換o2後,程式P的行為功能不變,則S是T的子型別。(What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T. )。她剛剛獲得了2004年度的馮·諾依曼獎。美國工程院和藝術科學院的雙院士。並且在麻省理工擔任軟體程實驗課程的教學。)
(LY注:LSP讓我們得出一個非常重要的結論:一個模型,如果孤立的看,並不具有真正意義上的有效性。模型的有效性只能通過它的客戶程式來表現。在考慮一個特定設計是否恰當時,不能完全孤立地來看這個解決方案。必須要根據該設計的使用者作出的合理假設來審視它。)
7.1 美猴王的智慧
一個例子,孫悟空勾生死簿。對猴類作的操作,對它的子類石猴和獼猴都適用。
7.2 什麼是里氏代換原則
里氏代換原則
定義:如果對每一個型別為T1的物件O1,都有型別為T2 的物件O2,使得以T1定義的所有程式P在所有的物件O1都代換為O2時,程式P的行為沒有變化,那麼型別T2是型別T1的子型別。
即,一個軟體實體如果使用的是一個基類的話,那麼一定適用於其子類。而且它覺察不出基類物件和子類物件的區別。(LY注:此處我仍有些糊塗。這種多型的性質,應該是由後期繫結機制保證的。而後期繫結應該是物件中儲存了某些型別資訊吧。這樣即使進行轉型,仍然根據物件可以判斷出正確的型別,每種語言的後期繫結不完全一致,但這應該是相同的。各語言具體的後期繫結機制的細節,有時間要了解一下。)
反過來的代換不成立
反之,如果一個軟體實體使用的是一個子類的話,那麼它不一定適用於基類。(LY注:因為子類往往是對基類的擴充套件,所以子類的介面可能會比基類寬。窄化是危險的。PS:“將某個object reference視為一個reference to base type”的動作叫做向上轉型(upcasting)。)
Java語言對里氏代換的支援
Java語言會在編譯期進行檢查,假設子類中實現了一個在基類中宣告的方法,如果基類中的訪問許可權是public,那麼子類不能降低它的訪問許可權。
因為,如果有客戶端程式呼叫父類的public方法,而用子類代替父類時,因為降低了訪問許可權,客戶端程式不能再繼續呼叫。這樣就違反了里氏代換原則。
Java語言對里氏代換支援的侷限
Java語言對里氏代換的支援是有侷限的。
就象描述一個物體大小的量有精度和準確度兩種屬性。精度是指量的有效數字有多少位;準確度則是指這個量與真實的物體大小相符合到什麼程度。一個量可以有很高的精度,但是卻無法和真實的物體情況相符合。Java編譯器能檢查的,就像是精度一樣,它無法檢查這個量與真實物體的差距。
Java編譯器不能檢查一個系統在實現和商業邏輯上是否滿足里氏代換法則。(LY注:語法上可以檢查,語義上不能區分。)
一個典型的例子:正方形是不是長方形的子類。
7.3 里氏代換原則在設計模式中的體現
策略模式(Strategy)
如果有一組演算法,那麼就將演算法封裝起來,使得它們可以互換。
客戶端依賴於基類型別,而變數的真實型別則是具體策略類。這是具體策略焦色可以“即插即用”的關鍵。
合成模式(Composite)
合成模式通過使用樹結構描述整體與部分的關係,從而可以將單純元素與符合元素同等看待。由於單純元素和符合元素都是抽象元素角色的子類,因此兩者都可以替代抽象元素出現在任何地方。
里氏代換原則是合成模式能夠成立的基礎。
代理模式(Proxy)
代理模式給某一個物件提供一個代理物件,並由代理物件控制對原物件的引用。代理模式能夠成立的關鍵,就在於代理模式與真實主題模式都是抽象主題角色的子類。客戶端只知道抽象主題,而代理主題可以替代抽象主題出現在任何需要的地方,而將真實主題隱藏在幕後。
里氏代換原則是代理模式能夠成立的基礎。
7.4 墨子論“取譬”
《墨子》有大取小取兩章。“取”是“取譬”的意思。用面向物件的語言來解釋,“取譬”研究的就是類和類的例項。(LY注:覺得墨子是諸子中最喜歡物理的一位,讓我想起亞里士多德。)
白馬與馬
《墨子 小取》中說,“白馬,馬也;乘白馬,乘馬也。驪馬,馬也;乘驪馬,乘馬也。”驪馬是黑色的馬。墨子說,不論黑馬、白馬均是馬的一種。既然馬可以騎,那麼白馬和黑馬必可騎。
馬是抽象的馬,白馬和黑馬是馬的具體子類。一個操作如果適用於馬,必然適用於黑馬和白馬。基類可以出現的地方,一定可以替換為子類。
反過來的代換不成立
《墨子 小取》中說,“娣,美人也,愛娣,非愛美人也….盜,人也;惡盜,非惡人也。”妹妹雖然是美人,但喜歡妹妹並不代表喜歡美人。盜賊是人,但討厭盜賊也並不代表就討厭人類。
7.5 從程式碼重構的角度理解
當兩個具體類A和B之間的關係違反了里氏代換原則是,重構的方案有兩種:
(1) 建立一個新的抽象類C,作為兩個具體類的父類,將A和B的共同行為移動到C中,從而解決A和B行為不完全一致的問題。
(2) 把B繼承自A改為委派關係。(LY注:改為用組合實現,參看合成/聚合原則)
長方形和正方形
正方形是否是長方形的子類的問題,西方一個很著名的思辨題。
正確的寫法是:
長方形類:兩個屬性,寬度和高度;正方形類:一個屬性,邊。
(LY注:這是至少流行了十年的思辨題目,最早來自於C++和Smalltalk領域。類似的這種思辨問題還有哪些呢?讓我不禁對哲學又感冒起來了。查閱資料時意外找到了一個討論區,裡面有讀者和作者關於此處的拓展討論,真讓人高興。)
(LY注:書中沒有提契約即Design by Contract的概念。子類應當完全繼承父類的contract。《敏捷軟體開發:原則、模式與實踐》一書中這樣寫,"基於契約設計(Design By Constract),簡稱DBC"這項技術對LISKOV代換原則提供了支援.該項技術Bertrand Meyer伯特蘭做過詳細的介紹:使用DBC,類的編寫者顯式地規定針對該類的契約.客戶程式碼的編寫者可以通過該契約獲悉可以依賴的行為方式.契約是通過每個方法宣告的前置條件(preconditions)和後置條件(postconditions)來指定的.要使一個方法得以執行,前置條件必須為真.執行完畢後,該方法要保證後置條件為真.就是說,在重新宣告派生類中的例程(routine)時,只能使用相等或者更弱的前置條件來替換原始的前置條件,只能使用相等或者更強的後置條件來替換原始的後置條件。 本書中長方形的Contract是width和height可以獨立變化,這個contract在正方形中被破壞了。)
(LY注:注意,我們所有討論的基礎都應由類的行為決定。這使得長方形等類是動態的,而不是象現實生活中一樣是靜態的概念。)
正方形不可以作為長方形的子類
如果設定一個resize方法,一直增加長方形的寬度,直到增加到寬度超過高度才可以。
那麼如果針對子類正方形的物件呼叫resize方法,這個方法會導致正方形的邊不斷地增加下去,直到溢位為止。換言之,里氏法則被破壞掉了。
這個例子很重要,它意味著里氏代換與通常的數學法則和生活常識有不可混淆的區別。
(LY 注:常識認為,正方形is a 長方形,而且是一類特殊的長方形。但是在這裡出了問題,如果我們系統中不會有這樣的resize操作,是否正方形就可以作為長方形的子類了呢?看後文是可以的)
程式碼的重構
長方形和正方形到底應該是什麼關係呢?
它們應該都是四邊形類的子類。四邊形類中沒有賦值方法,因類似上文的resize()方法不可能適用於四邊形類,而只能只用於不同的具體子類長方形和正方形。因此里氏代換原則不會被破壞。(LY注:針對需要賦值操作的情況)
從抽象類繼承
應儘量從抽象類繼承,而不是從具體類繼承。
上文對長方形和正方形的重構使用了重構的第一種方法。增加一個抽象類,讓兩個具體類都成為抽象類的子類。
記住一條指導性的原則,如果有一個由繼承關係形成的等級結構的話,在等級結構樹圖上的所有樹葉節點都應當是具體類;而所有的樹枝節點都應當是抽象類或者Java介面。
問答題
1、 一個有名的思辨題,filename能不能作為string類的子類?
答:不能。Filename物件不能實現string物件的所有行為。比如兩個string物件相加可以給出一個新的有效的string物件。而兩個filename物件相加未必會得到一個新的有效的Filename物件。
另外,Java中的String型別是final型別,因此不可以繼承。
2、 如果正方形的邊長不會發生改變,是否可以成為長方形的子類呢?(LY注:不變正方形,就是邊長不會發生變化的正方形,也就是遵守不變模式的正方形。不變(Immutable)模式,一個物件在物件在建立之後就不再變化。)
答:可以。實現時,父類有兩個屬性寬度和高度。子類有三個屬性寬度、高度和邊。針對每一個屬性,包含一個內部變數,一個Set值方法,一個Get值方法。子類正方形只需要將Set值方法不寫任何語句即可。
3、 從裡式代換角度看Java中Properties和Hashtable的關係是否合適?
答:不合適。在Java中,Properties是Hashtable的子類。顯然Properties是一種特殊的Hashtable,它只接受string型別的鍵(Key)和值(Value)。但是,父類Hashtable可以接受任何型別的鍵和值。這意味著,在一些需要非String型別的鍵和值的地方,Properties不能取代Hashtable。
(LY注:合成/聚合複用原則中有更詳細的討論,應使用合成/組合而不是繼承。它們是has a的關係而不是is a的關係。)