標籤:
如果類的介面不是內聚的,就表示該類具有“胖”介面。換句話說,類的“胖”介面可以分解成多組方法。每一組方法服務於一組不同的客戶程式。
ISP承認有一些對象確實需要有非內聚的介面,但是ISP建議客戶程式不應該看到它們作為單一的類存在。相反,客戶程式看到的應該是多個具有內聚介面的抽象基類。
介面汙染
考慮一個安全系統。在這個系統中,有一些Door對象,可以被加鎖和解鎖,並且Door對象知道自己是開著還是關著。這個Door編碼成一個介面,這樣客戶程式就可以使用那些符合Door介面的對象,而不需要依賴於Door的特定實現。
public interface Door{ void Lock(); void Unlock(); bool IsDoorOpen();}
現在,考慮一個這樣的實現,TimedDoor,如果門開著的時間過長,它就會發出警報聲。為了實現這一點,TimedDoor對象需要和另外一個名為Timer的對象互動。
public class Timer{ public void Register(int timeout,TimerClient client) {}}public interface TimerClient{ void TimeOut();}
如果一個對象希望得到逾時通知,它可以調用Timer的Register函數。該函數有兩個參數,一個是逾時時間,另一個是指向TimerClient對象的引用,其TimeOut函數會在超市到達時被調用。
如何把TimerClient類和TimedDoor類聯絡起來,才能在逾時時通知TimedDoor中相應的處理代碼呢?
常見的解決方案是,其中Door繼承了TimerClient,因此TimedDoor也就繼承了TimeClient。這就保證了TimerClient可以把自己註冊到Timer中,並且可以接收TimeOut訊息。
這種做法的問題是,現在Door依賴於TimerClient了。可是並不是所有種類的Door都需要定時功能。事實上,最初的Door抽象類別和定時功能沒有任何關係。如果建立了無需定時功能的Door的衍生類別,那麼在這些衍生類別中就必須要提供TimeOut方法的退化實現,這就可能違反ISP。此外,使用這些衍生類別的應用程式及時不使用TimerClient類的定義,也必須要引入它。這樣就具有了不必要的複雜性以及不必要的重複性的臭味。
分離客戶就是分離介面
Door介面和TimerClient介面是被完全不同的客戶程式使用的。Timer使用TimerClient,而操作門的類使用Door。既然客戶程式是分離的,所以介面也應該保持分離。
不應該強迫客戶程式依賴並未使用的方法。
如果強迫客戶程式依賴於那些它們不使用的方法,那麼這些客戶程式就面臨著由於這些未使用的方法的改變所帶來的變更。這無意中導致了所有客戶程式之間的耦合。換種說法,如果一個客戶程式依賴於一個含有它不使用的方法的類,但是其他客戶程式卻確實要使用該方法,那麼當其他客戶要求這個類改變時,就會影響到這個客戶程式。我們希望儘可能地避免這種耦合,因此我們希望分離介面。
類介面和對象介面
再次考慮一下TimedDoor。它具有兩個獨立的介面,被兩個獨立的客戶-Timer以及Door的使用者-使用。因此實現這兩個介面需要操作同樣的資料,所以這兩個介面必須在同一個對象中實現。那麼怎樣才能遵循ISP呢?怎樣才能分離必須在一起實現的介面呢?
那就是一個對象的客戶不必通過該對象的介面去訪問它,也可以通過委託或者通過該對象的基類去訪問它。
通過委託(適配器)分離介面
一個解決方案是建立一個派生自TimerClient的對象,並把對該對象的請求傳遞給TimedDoor。
當TimedDoor想要向Timer對象註冊一個逾時請求時,它就建立一個DoorTimerAdapter並且把它註冊給Timer。當Timer對象發送TimeOut訊息給DoorTimerAdapter時,DoorTimerAdapter把這個訊息傳遞給TimedDoor。
這個解決方案遵循了ISP原則,並且避免了Door的客戶程式和Timer之間的耦合。即使對Timer進行了改變,也不會影響到任何Door的使用者。此外TimedDoor不必具有和TimerClient一樣的介面。DoorTimerAdapter將TimerClient介面轉換為TimedDoor介面。因此,這是一個通用的解決方案。
不過,這個解決方案還是有些不太優雅。每次想去註冊一個逾時請求時,都要去建立一個新的對象。同時,類型轉換會倒置一些很小但仍然存在的已耗用時間和記憶體的開銷。
使用多重繼承分離介面
在這個模型中,TimedDoor同時繼承了Door和TimerClient。儘管這兩個基類的客戶程式都可以使用TimedDoor,但是實際上卻都不再依賴於TimedDoor。這樣,它們就通過分離的介面使用同一對象。
通常都會優先使用這個解決方案。
結論
胖類會導致它們的客戶程式之間產生不正常的並且有害的耦合關係。當一個客戶程式要求該胖類進行一個改動時,會影響到所有其他的客戶程式。因此,客戶程式應該僅僅依賴於它們實際調用的方法。通過把胖類的介面分解為多個特定於客戶程式的介面,可以實現這個目標。每個特定於客戶程式的介面僅僅聲明它的特定客戶或者客戶組調用的那些函數。接著,該胖類就可以繼承所有特定於客戶程式的介面,並實現它們。這就解除了客戶程式和它們沒有調用的方法間的依賴關係,並使客戶程式之間互不依賴。
摘錄自:[美]RobertC.Martin、MicahMartin著,鄧輝、孫鳴譯 敏捷式軟體開發 (Agile Software Development)原則、模式與實踐(C#版修訂版) [M]、人民郵電出版社,2013、115-121、
敏捷式軟體開發 (Agile Software Development) – ISP 介面隔離原則