類別繼承、介面宣告與模組混成(mix-in)
在思考 Ruby 模組與混成(mix-in)概念的過程中,勾起了我當初學習 Java 的記憶。C++ 藉由多重繼承達成程式碼再用之目的,也因此衍生了類別鑽石繼承問題。而 Java 出現時,強調它使用單一繼承並結合介面宣告而避免鑽石繼承問題。然而我對介面的使用經驗卻是負面的。
介面只宣告行為的外觀而不牽涉細節,細節在類之中個別實現。舉例而言,如果有兩個不具共同父祖類別的類,假設為 A, B 類,但具有一個共同的行為、一段相同的程式。 C++ 的作法是將此共同行為 - 亦即這一段相同的程式 - 設計為一個類,假設為 C 類,再令 A, B 類多重繼承 C 類;只要 C 類之中沒有任何屬性與 A, B 類之父祖相同,就不會導致鑽石繼承,同時達成程式碼再用之目的。Java 的作法則有兩個方式,其一是介面,其二是深度繼承。
介面的設計方式是以介面宣告 A,B 類具有共同行為 c ,接著在 A, B 類之中分別實現 c 的細節。具體而言,先在 A 類寫出 c 的程式碼,然後再把這一段程式碼複製到 B 類。「複製」就是介面宣告的最大缺點。介面只宣告外觀而無細節,所以它不能再用程式碼。如果我們有許多類別皆具有此共同行為,我們就要將同一段程式碼複製許多份。熟悉 XP/Agile methods 的程序員此時一定會大叫 DRY! (Don't Repeat Yourself)。程式碼的剪貼複製絕對是滋養 bug 的溫床。假設我們日後發現這段程式碼有一個 bug ,我們當然要修正。但若它已經為了實現介面內容而被剪貼複製到許多類別之中,修正它就是個大麻煩。誰知道它被複製到多少類別中了?
深度繼承的設計方式就是設法拉關係。一樣先將共同行為設計為一個類, C 類。接著從 A,B 的父祖輩中拉關係,將 C 類加入 A, B 的繼承樹中,亦即令 C 成為 A, B 的父祖之一。在單一繼承機制下拉關係動作非常麻煩,可能要翻出祖譜來上找十八代才找得到關係。如果我們要查看 A,B 類之共同行為 c 的實作內容,我們可能要上溯十八個父祖類才能在第十八代祖宗的 C 類之中找到行為 c 的內容。
我描述一個極端情形,我把每一個行為都單獨設計成一個類,那麼一個具有18種行為的類,就必須繼承十八代祖宗才能實現。說到這我想到結構化設計中的功能樹了,在深度繼承下的繼承樹,根本就是一棵倒過來的功能樹。結構化設計的功能樹是將功能不斷往下細分到最小功能,因此一個完整的功能就是由底下所有小功能組合而成。把這棵樹倒過來,就成了深度繼承樹:功能最豐富的子類,是自其上所有父祖類別繼承而得。那麼,以往結構化設計時碰得到的問題,在此一樣會出現。
當年在學 Java 時,我便感覺到介面宣告規避了鑽石繼承問題,卻無助於提高程式碼再用性。 Java 的介面宣告,只是規避而非解決鑽石繼承問題。實務上,鑽石繼承問題也不是那麼容易就碰得到, C++ 的多重繼承一直都好好地運作著。在動態語言中,雖然多數都採用單一繼承機制,卻又毫不死板,憑藉著動態性提供繼承機制以外的程式碼再用方式。 Ruby 的混成(mix-in)機制就是一個聰明的例子。這個概念也開始被程序員應用於其他語言之中,例如我嘗試於 JavaScript 和 PHP 中實踐混成概念 (PHP 實踐 mix-in 概念之可行性)。隨著混成概念於其他語言之實踐方式逐漸完善,對於「繼承」的想法與實踐途徑必然會更加豐富,設計方式也將更具彈性。
樂多舊回應