在前兩回中我們陸續介紹了「相依倒置原則(Dependency Inversion Principle,DIP」以及「控制反轉(Inversion of control,IoC)」原則。

我們可以說,以上所提到的這兩種設計原則,都是為了控制「改變」對整體程式碼所影響的範圍,盡可能將影響的範圍縮小到局部。軟體設計及開發的主要天敵之一就是「改變」,但「改變」又是難以避免的,因此,如何透過設計,來妥善因應「改變」,就成了設計時的重要課題之一。

導入專職管理角色
「控制反轉」本身是個設計原則,但是,在實作上,我們要怎麼達到這個原則的目標呢?你可以想想,在「控制反轉」中共有三種角色,分別為:(1)希望和實作無關的通用部份(2)抽象層中的介面和(3)實作相關的特定部份。

而在「控制反轉」中,我們希望通用部份只認識抽象介面,而與特定部份無關。不過,問題來了,即使通用部份的程式碼只操作抽象的介面,但是,最終此抽象的介面還是得繫結到真正的實作品。也就是說,和實作相關的特定部份會被經由某種方式產生出來,接著再以抽象介面的形式交付給通用部份,這使得通用的部份可以僅經由抽象介面來認識實作品、而又不和實作產生相依關係。

從上述的描述不難理解,想要達到「控制反轉」,實作時就必須處理「透過某種方式產生和實作相關的特定部份,再將它以抽象介面的形式交付給通用部份」的議題。有很多方法都可以達到這個目的,例如,著名的生成模式Factory Method,便是基於這個目的而被設計出來的。

在Factory Method設計模式中,透過導入另一個負責生產產品的「工廠」角色,來達到這個目的。在Factory Method設計模式,有個「工廠」能生產符合特定「介面」的「實際產品」。對於工廠的客戶而言,它只需要求工廠提供產品,並不需要理會這個產品究竟是如何實作的,工廠會保證客戶所得到的產品,都符合所有產品都應該具備的介面。這麼一來,客戶端的程式碼就會實作無關,因為它所操作的都是介面,而不是額外的實作細節,這就達到我們控制反轉的目的了。

除了Factory類型的設計模式用來達到「控制反轉」的目的之外,像Service Locator設計模式,也是用來達到同樣的目的。

在Service Locator模式中,一樣有個客戶想使用某種服務,而這個服務是有個共通的抽象介面所定義,也就是說,可能有多個服務但都遵守同一個抽象介面。當客戶想要使用服務時,它可能會告訴Service Locator它需要的是何種服務,而Service Locator便會自動回傳滿足客戶需求的服務,客戶並不知道該服務具體的實作為何,但可以確定的是,此服務符合所有服務都該具備的介面,而且滿足當初它所告訴Service Locator的條件。透過這個模式中導入的Service Locator,就可以達到「控制反轉」設計原則的目的。

不論是Factory Method或是Service Locator,都是透過導入另一個專職管理各實作品的角色,來隔絕客戶程式與實作品,讓客戶只能透過介面來接觸實作品、而無法碰觸到實作品本身。

在這手法下,等於將介面和實作間的繫結關係,封裝在這個專職管理實作品的角色中,日後,不論是要增加新的實作品,或是現有的實作品必須有所變動,影響層級也會局限在這個管理的角色裡,而不會波及客戶端的程式碼。這就是本文一開始所提到「盡可能將影響的範圍縮小到局部」的意思。

將相依性注入實作中
除了導入另一個專職管理之角色的手法之外,想要實作「控制反轉」,還有一類方式,就是所謂的「相依性注射(Dependency Injection,DI)」。相依性注射也是一個設計模式,之所以會有這樣的名稱,是因為在這個模式中,當需要建立相依性關係時,乃是透過抽象介面來將所相依的實際物件予以注射進去。

還記得在「控制反轉」的原則中的三個角色嗎?就是依賴者(通常是通用的部份)、介面、以及被依賴者(通常是特定的部份)。簡單來說,我們就是想盡辦法要讓通用的部份透過介面來運用特定的部份,而注射相依性,就是將特定的部份透過介面,注射到通用的部份去。

在Martin Fowler首次提出「相依性注射」這個名詞的文章中,將相依注射的方式區分為三類,分別為:Type 1──介面注射(interface injection)、Type 2──設定式注射(setter injection),以及Type 3──建構式注射(constructor injection)。

而這三種的注射方式的差別,便是讓依賴者取得代表被依賴者之介面的方式不同。

在設定式注射中,是透過呼叫依賴者所具備的設定式(setter),將被依賴者的實作以介面的形式傳入,而依賴者便可得到此一介面,接著在自身的程式碼中加以運用。而在建構式注射中,此介面則改由依賴者的建構式中傳入,而非由其設定式傳入。建構式注射的應用方式,主要是在建構依賴者的同時,便同步建立好相依性的關係。而設定式注射的時間點則較有彈性,不限於在建構依賴者的同時就必須完成關係的建立,而可以透過呼叫設定式的時間點來加以控制。

你可以發現,採用相依性注射的方式之下,依賴者得到代表被依賴者介面的方向,恰好和使用Factory Method或Service Locator相反。在相依性注射下,依賴者是被動的告知相依性關係,而在後者中,無論是Factory Method或Service Locator,都是由依賴者主動地尋求另一個專職管理的角色,來提供代表被依賴者的介面。而這兩種方式,都可以達到控制反轉的目的。

在前一段中,我們故意略過了介面注射,這是因為它其實比較複雜的關係。怎麼說呢?

無論是透過設定式或建構式來注射相依性關係,都是透過函式這個層次來進行注射。但是,介面注射則是把注射的方式提高到介面的層次,也就是說,我們會另行制定一個抽象介面,讓依賴者去實作,在這個介面中定義了一些函式(可能像是設定式),是所有的依賴者都必須實作的。當要進行注射時,因為依賴者具有這介面中的所有函式,所以便可以透過這些函式來注射。

為什麼需要像介面注射這種複雜的方式呢?用設定式注射不就好了嗎?你應當可以留意到,無論是設定式注射或建構式,依賴者(也就是即將要使用介面的角色)的類別名稱在程式碼中寫明的,這使得你無法輕易抽換依賴者。相反的,若是你採用介面注射,無論是依賴者或被依賴者,都是可以輕易抽換的,因為二者都是以介面來描述!一個介面用來抽象化注射方式,而另一個介面則用來抽象化相依性關係。而注射方式本身又正是主程式對依賴者的相依性關係。

那麼,既然二者都可以被抽換,那主程式又要如何得知究竟程式中的依賴者是誰?而被依賴者又是誰呢?主程式可以倚靠外部的設定檔來決定。例如,在主程式啟動的時候,透過讀取設定檔,知道依賴者以及被依賴者的類別名稱,接著動態地載入二者。因為主程式僅會透過介面來操作依賴者及被依賴者,所以它們也就可以更換成任意實作此二介面的類別,因而得到極為高度的彈性。

之後還有更多相依性注射的型態陸續被提出,但總體精神都是一樣的,便是主動依賴者得到被代表被依賴者的介面。

 

作者簡介


Advertisement

更多 iThome相關內容