今天要記錄的是介面隔離原則,顧名思義是和介面高度相關的原則。因此在閱讀本篇之前,可能需要先對 介面 有一點了解呦。

事情就從上一篇 里氏替換原則 的鳥類物流公司開始說起。老闆痛定思痛,決定先用介面先規定好物流士們的應徵條件,例如裝貨、卸貨、飛行、必須有帥氣的喙等等。

這道命令下來後,倉庫們的企鵝都慌了,來檢查的編譯器瘋狂跳出 Error:「您未實作 IBird 的 Fly() 方法!」這下怎麼辦呢,為了要保住飯碗,企鵝們就必須實作出飛行才行,可是企鵝真的就不會飛呀!

這下子企鵝們只剩下兩個選擇:不實作飛行,但是就不能被當成物流士,最後就會被開除;或是……空實作。

public class Penguin : IBird
{
    public void Fly()
    {
      // Do nothing;
    }
}

企鵝們終於騙過了編譯器檢查員,然而當送貨的命令下來之後,企鵝們再一次卡在倉庫門口發呆,最終物流公司仍然踏上了虧損的老路,再度面臨倒閉危機…

介面隔離原則(Interface Segregation Principle)

不知道大家對企鵝遇到的狀況有沒有經驗呢?

當介面規定了太多要求,而我們實作的子類別只需要其中一部份,或是有些要求根本無法達成,就會發生這個困境:放棄實作介面,或是用空實作和錯誤處理去欺騙介面

例如資料庫存取的介面要求太多和當下資料庫過於一致的方法,結果替換資料庫的時候導致部份方法實作不出來;或是像 俺同事文章 中的例子,交通工具的介面要求能開關車門,結果電動機車無法實作。

然而,如果我們選擇用空實作或是拋出錯誤的方式,去欺騙介面,等到需要呼叫該方法的時候,就會發生許多非預期的錯誤。甚至讓接手程式碼的人在什麼都不知道的情況之下就讓系統掛掉。聰明的朋友們一定發現了,這就是違反了 里氏替換原則

為了迴避到處都是空實作地雷的結局,大前輩們就提出了介面隔離原則:

不應該強迫用戶依賴它們未使用的方法

這邊的用戶也就是我們的子類別,它們等同是這個介面的使用者。當我們必須強迫使用者去實作一些他們不需要的方法時,就代表了一個事實:我們的介面太「胖」了!裡面的某些要求可能是非必要的,以至於造成了實作上的冗餘。

也基於這條原則延伸出了一個方向:應該最小化類別與類別之間的介面

介面也要單一職責

但是,我們要怎麼知道是我們設計的介面太胖,還是子類別在偷懶呢?又要怎麼知道我們的介面設計是否已經「最小化」呢?

那就是 單一職責原則 出場的時候了。一個合理的介面設計是能夠符合單一職責原則的,反過來說,我們可以用單一職責原則來檢視我們的介面設計是否良好。

當我們設計介面的時候,或是像上面遇到必須空實作的時候,就可以思考一下:這個介面的職責是否單一?這個介面的意圖是什麼?這個介面是否只對一個角色負責、只有一個原因改變?

當我們的介面符合單一職責、足夠 內聚 的時候,我們自然就能夠說這個介面已經足夠精簡了。

用組合實現功能

有些人可能就會有點疑惑了:「但是我就是需要這個功能呀,如果我不塞在介面,要放去哪呢?」

很簡單,放去另一個該職責的介面就可以了

和繼承需要注意的部分一樣,濫用介面也是濫用繼承,我們應該用組合去實現功能而不是用繼承去綁死功能。一個資料串列能做的功能可能相當多,但我們並不需要一次就要求實現全部能做的事情,而是將這些工作分組,再從中組合出我們需要的部份。

此處以 C# 來說,例如我們很常接觸的 List 類別,並不是只實作了 IList,而是實作了 ICollection<T>, IEnumerable<T>, IEnumerable, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection, IList 這些介面,層層堆疊,相互嵌合。

那我們就知道 List 由這些介面「組合」而成,也知道了我們 List 具有這些介面要求的能力,讓我們能在合適的時候使用這個類別。同時又保留了這些介面能搭配出另一種組合的彈性,例如 Array 就是由 ICollection, IEnumerable, IList, IStructuralComparable, IStructuralEquatable, ICloneable 組合而成。

就像可能有位大神,名片一拿出來就是一串「程式設計師/架構師/攝影師/貓奴」,我們的類別也要懂得斜槓。如此一來類別就比較不容易被介面綁死,也能因應不同場合來決定身分,從「每次都被逼著買套餐可是又不喜歡小菜」變成「餐餐自由配」,組合就該如此自由!

回到開頭的例子,把想要的行為全部定義在一個介面裡,然後用一個類別去實現它遇到不需要的動作就詐騙介面,是相當不 OK 的;而是應該把想要的行為用職責的角度去思考,根據職責建立成一或多個介面。然後只挑選並實作該類別需要的動作(介面),如此就可以讓介面不再臃腫,而是變得靈活。

就像是武術秘笈中的招式,其實也是一連串的動作所組成;所謂的功能,其實也是一連串行為所組成的。既然行為組合成了功能,我們也要從組合的角度去思考如何建立類別。組合就像是積木一樣,我們用積木堆疊來完成作品,同時每個積木又可以各自靈活運用。

而積木也分成了好用的積木,和很難使用的積木,在程式中可以從夠不夠 SOILD 看出來。不好用的那些用起來會覺得卡卡的,測試也很難寫;好用的則會讓你面對變化的時候,就像拆裝樂高一樣順手方便。聰明的朋友可能聯想到了,這就是 開放封閉原則 中我們提過的模組化。

通常來說積木的形狀越複雜、體積越大,就越難以靈活使用,介面也是如此,因此我們在設計介面的時候,要謹記介面隔離原則,利用我們在單一職責原則、里氏替換原則學到的原則來檢驗我們的介面,如此就可以迴避相當多尷尬兩難的實作場面,也能讓介面的使用更加靈活。

那麼,在結束之前,有興趣的朋友可以跟我一起想一想:介面隔離原則,只適用於設計類別架構時的介面嗎?其他的介面(Interface)呢?例如 API,是不是也可以按照介面隔離原則的精神下去設計呢?

後日談

企鵝詐騙介面的事情終究還是暴露了。

但是這群企鵝的夢想就是成為物流士,老闆也狠不下心把牠們趕走。

「也許……」鴨子顧問說:「我們可以有別的方法。只要使用介面隔離原則。」

老闆:『介面隔離?怎麼做呢?』

「我們可以把送貨放到 IDelivery,然後讓他們用不同的介面來實作移動方式,例如 IFly、ISwim、IRun 等等。用組合的方式來完成不同種類的物流士類別,這樣就可以有很多種送貨方式了」

『原來如此,不只是空運 —— 我們要征服陸海空嗎!』

改變作法的鳥禽物流公司搖身一變成了動物物流公司,同時廣徵天下動物,除了企鵝也能從南極發貨中心快速運貨以外,公司還招到了明星成員獵豹物流士,從此蒸蒸日上、強勢打入各大物流市場,最後進軍宇宙。可喜可賀,可喜可賀。

本系列下一篇:菜雞與物件導向 (14): 依賴反轉原則

參考資料

同系列文章