隔絕相依性的方式
Last updated
Was this helpful?
Last updated
Was this helpful?
參考資料:()
將物件相依的介面,拉到建構式供外部物件使用,可自行組合目標物件的相依實體。屬性為private。
有許多DI framework 支援 Autowiring.
Autowiring is an automatic detection of dependency injection points.
這邊的 dependency injection points 在這例子,指的就是建構式。
由建構式傳入相依介面的實體物件,是一個很通用的方式。因此在結合許多常見的 DI framework,不需要再額外處理。
(以 Unity 為例,在 UnityContainer 取得目標物件時,會自動尋找目標物件參數最多的建構式。並針對每一個參數的型別,繼續在 UnityContainer 中尋找對應的實體物件,直到目標物件組合完畢,回傳一個完整的目標物件。)
當物件越來越複雜時,建構式也會趨於複雜。倘若沒有 DI framework 的輔助,則使用物件上,面對許多 overload 的建構式,或是一個建構式參數有好幾個,會造成使用目標物件上的困難與疑惑。若沒有好好進行 refactoring,也可能因此而埋藏許多 bad smell。
另外,倘若是許多建構式,也可能造成要呼叫 A 方法時,應選用 A 對應的建構式,但在使用物件上,可能會用錯建構式而不自知,若方法中沒有正確的防呆,則可能出現錯誤。(請搭配單元測試的測試案例來輔助)
最後,與原本直接相依的程式碼相比較,目標物件的相依物件因此暴露出來,交由外部決定,而喪失了一點封裝的意味。而使用端也不一定知道,要取用此物件時,應該要注入哪些相依物件。(請使用 Repository Pattern 或 DI framework 來輔助)
其實公開屬性與公開建構式非常類似,透過 public 的 property(property 型別仍為 interface),讓外部在使用目標物件時,可先 setting 目標物件的相依物件,接著才呼叫其方法。
而公開屬性通常只會將 setter 公開給外部設定,getter 則設定為 private。原因很簡單,外部只需設定,而不需取用。就像公開建構式,在使用物件之前先傳入初始化物件必備的資訊,但目標物件可能將這些資訊,存放在 private 的 filed 或 property 中,而不需再提供給外部使用。
同樣的,public property 也是常見的 dependency injection points,所以也有許多 DI framework 支援。另外則是不需要對建構式進行改變,或增加新的建構式。對過去已經存在的 legacy code 的影響,會比建構式的方式小一點點(但幾乎沒有太大差異)。
最常見的情況,就是使用目標物件時,相依介面應有其對應執行個體,但卻因為使用端沒有設定 public property,導致使用方法時出現 NullReferenceException,這種情況也怪不了使用端,因為使用端極有可能本就不瞭解這個方法中,有哪些相依物件。
解決方式與建構式的建議雷同,首先當然要有測試程式來說明(測試程式就是物件使用說明書),另外取得目標物件,仍可透過 Repository Pattern,讓使用端無須瞭解目標物件的相依關係。
並且在方法中使用相依介面前,應檢查其是否為 null,若為 null,則代表參數設定錯誤,進行 error handling,避免已經發生錯誤仍執行許多不應執行的程式碼。或是在 property 的 getter 時,檢查是否為 null 或當為 null 時,給予一預設值,以避免方法無法正常執行。(視實際需求而定)
另外,公開屬性的方式,也如同公開建構式一般,破壞了一點點物件封裝的用意。但這兩者,都是 IoC 設計會帶來的影響。
既然前面兩種方式,都可能造成使用方法時,可能沒有設定好相依介面的執行個體,導致發生錯誤。或是使用目標物件時,不知道該呼叫哪一個建構式或初始化哪些屬性。那很簡單的方式,就是把方法相依介面的部分,拉到方法的參數上。方法中,需要使用到哪些介面,強迫由呼叫端必須給定參數。目標物件的方法內容則僅相依於參數上的介面。
不必再擔心要先初始化哪些 property,或呼叫哪一個建構式。當要呼叫某一個方法,其相依的物件,就是得透過參數來給定。基本上也不太需要擔心使用上造成困擾或迷惑。
最大的問題,在於方法簽章上的不穩定性。當需求異動,該方法需要額外相依於其他物件時,方法簽章可能會被迫改變。而方法簽章是物件導向設計上,最需要穩定的條件之一。以物件導向、介面導向設計來說,當多型物件方法簽章不一致時,向來是個大問題。
另外,方法的參數過多,在使用上也會造成困擾。而且會影響到 legacy code 的呼叫端,需要全面跟著異動,才能編譯成功。
而且透過參數的方式,DI framework 支援度較低。
但這不代表,就不能在方法參數中,傳入相依物件。在 .net framework 還是有許多這樣的設計,例如:List.Sort 方法 (IComparer)。這樣的設計方式,通常要確保該方法相依相當明確、穩固,避免上述問題。
by the way, 這個方式是可以與其他方式共存的,所以在設計物件時,可衡量搭配使用。
利用 protected virtual 去繼承欲測試的類別
這個方式最大的好處,是完全不影響外部使用物件的方式。僅透過 protected 與 virtual 來對繼承鏈開放擴充的功能,並且透過這樣的方式,就使得原本直接相依而導致無法測試的問題,獲得解套。
這是為了測試,且面對 legacy code 所使用的方式,而不是良好的物件導向設計的方式。IoC 的用意在於介面導向與擴充點的彈性,所以當可測試之後,倘若重構影響範圍不大,建議讀者朋友還是要將物件改相依於介面,透過 IoC 的方式來設計物件。
by the way, 同樣為了解決直接相依物件,甚至相依於 static 方法、.net framework 本身的物件(如 DateTime.Now)而導致無法測試的問題,還有另外一個方式,稱為 fake object。這在後面的文章,會再進行較為詳盡的介紹。