淺談ruby core library 與 Liskov Substitution Principle原則
Liskov Substitution Principle原則,簡稱LSP原則,是OOP軟體方法中的一個設計原則,其大意是:如果S是T的子類,那麼程式碼中所有用到T的地方,都可以通過S替代。這個原則是傳統的強型別面嚮物件語言(如Java)必須遵守的一條原則。關於LSP原則的更多介紹,參考wiki。
在ruby的基礎類庫中,Kernel Module是其核心,Object就是Kernel Module中的一個class,Object也是ruby中所有類的基類。在Object類中定義了一個dup方法,用來dump 物件資訊(包括物件名稱、ID等)。
現在的問題是,ruby中,一些繼承Object的subClass,會丟擲異常,當你呼叫dup方法時。
特別是在一些常量類(NilClass, FalseClass, TrueClass, Fixnum, 和 Symbol)會存在此問題。
看下面這個例子:
irb 1:0> 5.respond_to? :dup
=> true
irb 2:0> 5.dup
TypeError: can't dup Fixnum
from (irb):1:in `dup'
from (irb):1
irb 3:0>
如果你還不熟悉ruby,這裡解釋一下。第一行意思是測試型別為Fixnum的物件5是否存在dup方法(ruby中方法都是以Symbol物件儲存的,因此 :dup 表示 名稱為dup的Symbol物件),測試結果是true,因為Fixnum繼承自Object類。但是實際情況是呼叫dup方法拋異常。
所以,這就違背了LSP原則。有一個術語稱呼這種程式碼:Refused Bequest,通常解決這種問題的辦法是使用組合替代繼承。
在ruby社群有一個關於這個問題的討論,裡面提出了幾個解決方案。其中一種是將dup方法從常量類(如Fixnum)中刪除,這樣可以避免上面例子程式碼中的異常,因為5.respond_to? :dup返回的是false,但是這個還是違背了LSP原則。
這種行為在你做任意物件拷貝的場景下會出問題,可能你期望dup返回常量類自身,因為他是常量類,對吧?但是實際上這麼說也不是很準確,因為在ruby中你可以re-open一個類或物件,往其中新增或刪除方法。
因此,LSP原則到底意味著什麼?當我在blog中討論這個問題時候,Robert Dober,一位響應者,發表了觀點:
我想說的是LSP原則不適合ruby因為ruby中沒有提供類似這種的約定。為了闡述LSP我舉個例子,我們有一個Base類(請原諒我用Java)
void aMethod(final Base b){
....
}
然後我期望這個方法在任何我傳遞Base型別引數的場景下都執行正常,否則會編譯錯誤。
SubClass sc; // subclassing of Base
aMethod( sc ); // 這裡應當要執行正常
上面所說的約束(其實就是LSP),在ruby中並不存在。我相信ruby給我傳遞的資訊是:
- OO語言是一種面向類的語言。
- 動態語言是是一種面向物件的語言。
恕我直言,ruby改變了我很多面向類的觀點,更多的轉向了面向物件。
這位讀者的觀點是錯的,因為面向類只是Java編譯器做了檢查,實際上我們執行時候還是會有類似的異常,比如ClassCastException,你也可以理解Java為面向物件的。
作為一個Java程式設計師,對違背了LSP原則的做法總是會覺得很不爽,但是Ruby提供了很多方便又容易使用(相對與Java)的API,那違背一點LSP原則是不是也沒啥?
就像Robert Dober所說的,動態語言和靜態語言在設計上存在不少差異,在Ruby中你永遠不會使用is_a? 和 kind_of? api來檢查型別,而是遵循Duck Typing 哲學(其核心思想是:一個物件是看它能做什麼,而不是看它是什麼),所以ruby中通常依賴respond_to? api來判定一個物件是否可以做某個操作。
在這個dup例子中,更好的實現方法是常量類不實現dup方法,而不是拋異常,但是這個還是違背了LSP原則。
因此,我們能做到既遵守LSP原則,同時又有豐富的介面的基礎類和模組嗎?
現實中有很多例子來回答上面的問題:有些特性可以,有的不行。如你可能會問題,Java中為什麼每個class都要實現toString而不是toXML?(意思是假如Java中提供了和toString同等位置的toXML,相當於提供了更豐富的介面,但必定會違背LSP原則)。
從AOP(Aspect Oriented Programming)的角度來看,我更願意看到的結構是dup僅在支援此特性的類中有,不支援此特性的類沒有此方法。dup不應當是Kernel模組的基本特性,但是當需要使用它的時候必須是工作正常的。
實際上,在ruby的世界裡實現這種AOP很容易,或許Kernel、Module、Object應當拆分為很小的塊,然後根據實際的場景,用ruby中的混合(mixin)特性來組合他們。如下:
irb 1:0> my_obj.respond_to? :dup
=> false
irb 2:0> include 'DupableTrait'
irb 2:0> my_obj.respond_to? :dup
=> true
irb 4:0> def dup_if_possible items
irb 5:1> items.map {|item| item.respond_to?(:dup) ? item.dup : item}
irb 6:1> end
...
上面例子程式碼要表達的意思是:在Object類中不提供dup的抽象,而是在DupableTrait給每個可以支援dup的類加上dup方法(通過mixin),通過這種方法,可以解決違背LSP原則的問題,並且簡化了Kernel中類和模組的實現。因此我們說ruby中違背了LSP原則,但是ruby也提供了靈活的方法讓你遵循LSP原則,到底是遵循還是違背,要看實際應用場景。