通過對前面幾個設計原則的討論,我們會發現接口是一個好東西,因為使用接口,可以使代碼靈活性和可維護性更強。但是,在設計接口時,我們還必須遵循接口隔離原則。

接口隔離原則(Interface Segregation Principle,簡稱ISP)是指客戶不應該依賴它們用不到的方法,隻給每個客戶它所需要的接口。換句話說,就是不能強迫用戶去依賴那些他們不使用的接口。

接口隔離原則實際上包含瞭兩層意思:

  • 接口的設計原則:接口的設計應該遵循最小接口原則,不要把用戶不使用的方法塞進同一個接口裡。如果一個接口的方法沒有被使用到,則說明該接口過胖,應該將其分割成幾個功能專一的接口,使用多個專門的接口比使用單一的總接口要好。
  • 接口的繼承原則:如果一個接口A繼承另一個接口B,則接口A相當於繼承瞭接口B的方法,那麼繼承瞭接口B後的接口A也應該遵循上述原則:不應該包含用戶不使用的方法。反之,則說明接口A被B給污染瞭,應該重新設計它們的關系。

如果用戶被迫依賴他們不使用的接口,當接口發生改變時,他們也不得不跟著改變。換而言之,一個用戶依賴瞭未使用但被其他用戶使用的接口,當其他用戶修改該接口時,依賴該接口的所有用戶都將受到影響。這顯然違反瞭開閉原則,也不是我們所期望的。

下面我們舉例說明怎麼設計接口或類之間的關系,使其不違反ISP原則。

假如有一個門(Door),有鎖門(lock)和開鎖(unlock)功能。此外,可以在門上安裝一個報警器而使其具有報警(alarm)功能。用戶可以選擇一般的門,也可以選擇具有報警功能的門。分析需求,找出其中的名詞,我們不難得到三個候選類:門(Door)、普通門(CommonDoor)、有報警功能的門(AlarmDoor)。我們該如何設計這三個候選類之間的關系呢?

最簡單的設計就是將Door作為接口,在Door接口裡定義所有的方法,讓CommonDoor和AlarmDoor作為該接口的實現類,從而強制這兩個類實現Door接口中的所有方法,如下所示。但這樣一來,依賴Door接口的CommonDoor卻不得不實現未使用的alarm()方法。很顯然,這個Door接口有點肥胖,內聚性太差,違反瞭接口隔離原則。

太肥胖的接口

那麼,好吧,將報警功能從Door中分離出來,封裝成一個Alarm接口。在Alarm接口定義alarm()方法,在Door接口定義lock()和unlock()方法,Door接口繼承Alarm接口,如下圖所示。現在一個接口根據功能分成瞭兩個接口,貌似設計要更好一些。但是,可惜的是:跟第一種方法一樣,依賴Door接口的CommonDoor卻不得不實現未使用的alarm()方法,也就是說接口Door被接口Alarm“污染”瞭,這種設計同樣違反瞭接口隔離原則。

接口污染

很顯然,太肥胖的接口以及接口污染都會造成用戶依賴於他們不用的方法,從而違反瞭接口隔離原則。我們不應該強迫用戶依賴於他們不用的方法。那麼,到底如何做才能實現這一點呢?有兩種方式:

  • 通過多重繼承分離接口。多重繼承可以有兩個方式,第一種方式是同時實現兩個接口,屬於多重接口繼承;第二種方式是實現一個接口,同時繼承一個具體類,實際上也是一種多重繼承。現在我們繼續本例的設計,這次我們為瞭避免太肥胖的接口以及接口污染,我們在Alarm接口中定義alarm()方法,在Door接口中定義lock()和unlock()方法,這兩個接口之間無繼承關系。CommonDoor實現Door接口。而AlarmDoor根據多重繼承的實現方式,分為兩種方案。
  1. 第一種方案為:AlarmDoor類同時實現Door和Alarm接口,如下圖所示。

通過多重繼承分離接口方式一

  1. 第二種方案為:繼承CommonDoor,並實現Alarm接口,如下圖所示。這兩種設計方案都將Door接口和Alarm接口分離瞭,避免瞭肥胖的接口和接口污染,都遵循瞭接口隔離原則,但是第二種方案更具有實用性。

通過多重繼承分離接口方式二

  • 通過委托分離接口。在這種方法裡,AlarmDoor實現瞭Alarm接口,同時把功能lock和unlock委托給CommonDoor對象完成。這種設計遵循瞭接口隔離原則。實際上,這種方法是對第三種方法的第二個方案應用瞭組合/聚合復用原則,將AlarmDoor和CommonDoor的繼承關系轉換為聚合關系。

通過委托分離接口

接口隔離原則從對接口的使用上,為我們對接口抽象的顆粒度建立瞭判斷基準:在為系統設計接口的時候,使用多個專門的接口代替單一的胖接口。