在物件導向的設計中,多重繼承威力強大,然而,也隱藏著不少的問題。

長久以來,開發者一直在多重繼承的利與弊之間尋求平衡,可見的方式從小心翼翼地分開介面繼承與實作繼承,到使用Mixins來共用方法實作都有。然而,在Mixins仍舊會遭遇到多重繼承的一些老問題時,思考組合(Composition)一直都是個不錯的方向。

Mixins與多重繼承

表面上看來,現代有許多程式語言似乎都禁止多重繼承,在有明確繼承語義的語言中,像是Java的extends,只能指定一個父類別,然而,往往又會提供另一語法,可以從多個來源取得類別的定義,像是Java的implements,在JDK8以前,這多個來源的定義只能是沒有方法實作的介面,而在JDK8之後,多個來源的定義可以是不包含狀態、有方法實作的介面,除了Java之外,像Scala、Ruby等語言,都會有這類的機制,通常被稱為Mixins或Traits。

Mixins與Traits這兩個名詞,技術上來說其實有些不同,然而不同語言可能也會有不同的表現,因此在這邊暫且當它們是同義詞,重點在瞭解:Mixins、Traits實際上是一種在多重繼承與單一繼承間的妥協,是一種受限的多重繼承,在繼承來源為抽象的共用實作時,可避免多重繼承時的許多問題,然而,它並不能解決所有的問題,這部份可參考我先前專欄〈受限多重繼承的演進〉。

使用Mixins,一個顯見的問題是,仍舊有可能發生名稱衝突的問題。

另一個隱式的問題是,Mixins的來源與被Mixin的對象之間,實際上會建立起依賴關係,被Mixin的對象中,必須實現Mixins預期之協定,通常是某個或某些方法名稱,視情況而定。有時,這並不會有什麼問題,例如在Python中,類別定義時可以使用@total_ordering,並實作__eq__、__gt__方法,如此一來,就可以擁有<、<=、==、!=、>、>=這整組的豐富比較(Rich comparison)方法。

這是因為Python中,__eq__、__gt__這類協定,基本上是屬於語言層面的規範,因此@total_ordering背後的實作上,使用了Mixins是適當而且簡潔的方案,然而,若是開發者在實現Mixins時,自定義了一些協定,名稱上的隱式依賴就會是個問題,而在Java這類強型別語言中,因Mixins要求的方法被更名或移除時而造成的問題,或許還能靠編譯器來檢查出,然而在動態定型語言中,瞭解是否破壞Mixins的隱式協定,就會是個麻煩了。

如果Mixins依賴在某個或某些Mixins時,情況也會急劇地朝複雜發展,在〈Mixins Considered Harmful〉這篇文件中,就談到了Mixins會隨著時間演化,而導致雪崩式的複雜度。

關於對目標添加功能

Mixins的原理,某些程度上,就是對目標進行功能的添加,視語言的能力而定,方式可能是靜態或者是動態。

靜態的例子就像是Java中的實作介面,對象是類別,動態的例子就是直接對物件進行功能添加,因而在支援物件個體化的語言中,像是JavaScript或Ruby,經常看到Mixins運用,而以JavaScript來說,最簡單的實現方式之一,就是複製特性:

function mixin(dest, src) {
  for (var key in src) {
      dest[key] = src[key]
  }
}

這類動態為物件添加、修補功能的能力,在JavaScript中被視為很有彈性,許多框架也有類似的API來進行這類任務,然而這也是個雙面刃,就被修補的物件來說,添加功能實際上是一種侵入式的行為,物件被添加的功能越多,開發者需要掌握的事情就越多,像是一個類別被Mixin的來源越多,要知道一個方法從何而來,就越加困難。

談到動態地為物件添加功能這方面,在古老的設計模式中有個Decorator,就談到了「不採取繼承的方式,而以組合的方式……」,而在許多場合「相較於使用類別繼承,物件組合更好」這類的建議經常看到,Decorator的好處之一就是,每個物件仍然只要專心地做好自己的事情就好,被添加的功能是Decorator自己要負責的,Java中最常見的例子,就像是InputStream與BufferedInputStream這類的關係。

High Order Components

〈Mixins Are Dead. Long Live Composition〉中,標題就明確地談到了Mixins已死、組合萬歲。在React 0.13出來時,也明確地指出Mixins將會退場,理由包含了JavaScript中,在定義Mixins時並沒有一個標準或通用的方式,而支援Mixins的一些特性在ES6中也被剔除,繼續支援Mixins,並不符合ES6標準化的目的。

React改用了High Order Components(HOCs)的概念,來解決需要Mixins的一些場合。類似High Order Functions的概念,是指一個函式可以接受函式並傳回函式,而High Order Components是指一個函式可接受Components,並傳回Components,在〈Mixins Are Dead. Long Live Composition〉中舉的例子,技術上來說,是接受一個React.createClass定義的類別(一個Components),並傳回一個React.createClass定義的類別。

除了接受與傳回類別之外,與Mixins的範例相比,還有個重要的差異性是,在來源類別中,不用定義特定的協定名稱,而是傳入一個匿名函式來負責傳遞狀態,如果不熟悉React,這就好比:在Python,自定義了一個可以如下使用的@total_ordering:

@total_ordering(
  lambda self, other: self.radius == other.radius,
  lambda self, other: self.radius > other.radius
)
class Ball:
  def __init__(self, radius):
      self.radius = radius

上面的Python中,相當於Ball = total_ordering(..., ...)(Ball),total_ordering傳回的函式可接受類別,並傳回類別,傳回的類別擁有豐富比較方法,然而,我們可以看到:在Ball定義時,並不用定義名稱為__eq__、__gt__的方法,這部份的工作,是由呼叫total_ordering時傳入的兩個lambda來負責。

組合優於繼承

在上面的Python程式碼中,自定義的total_ordering有幾種實現方式。若要避免使用繼承,那麼,傳回的類別本質上會是個Decorator,一個簡單的示範程式碼,可參考我寫的Gist——Decorator可以一層一層套接,使用Python的話,就是@component1、@component2這樣一層一層宣告,原始類別定義了哪些程式碼,又被套接了哪些Components,顯然清楚多了。

ES7中可能也會有類似Python的decorator語法,不過,並不是用了decorator語法就是實現了High Order Components的概念,記得!除了有個可接受Components、並傳回Components的函式,更重要的是,思考「組合優於繼承」這個古老的原則。

作者簡介


Advertisement

更多 iThome相關內容