Unity 入門 (3)

上回,這次要看的是 Unity 解析物件型別的寫法。

在先前的「Hello, Unity!」範例中,我們已經了解 Unity 在解析物件的過程中自動挑選建構子的規則。可是在呼叫 Resolve 泛型方法時,我們傳入的 SayHelloInEnglish 類別是個具象型別(concrete type)。這種寫法的問題在於欠缺彈性。

DI 的重點是寬鬆耦合,而為了達成此目標,我們通常會事先建立抽象型別與具象型別之間的對應關係,並且把這個對應關係提供給 DI 容器(這裡就是 Unity)。如此一來,DI 容器在解析元件時,就會照我們的意思使用適當的型別來建立物件了。

接著還是延續上一個範例來說明如何以程式碼來建立這層對應關係。

TIP  如果你覺得「抽象型別」與「具象型別」過於拗口或有礙理解,閱讀時不妨試試把「抽象型別」換成「介面」,把「具象型別」換成「實作類別」。我自己也常這麼做。

抽象型別與具象型別之間的對應

記得上一個例子,我們的 SayHelloInEnglish 類別實作了 ISayHello。儘管有點牽強,還是姑且假設這是為了讓「打招呼」的動作能夠支援多國語言吧。現在,我們再增加一個用中文打招呼的具象類別:SayHelloInChinese。

    class SayHelloInChinese : ISayHello
    {
        public void Run()
        {
            Console.WriteLine("嗨! Unity 真好用!");
        }
    }

然後在應用程式的進入點撰寫程式碼來指定 ISayHello 介面要對應至哪個具象類別。所以原本的 Main 函式可以改成這樣:

        static void Main(string[] args)
        {
            var container = new UnityContainer();
            container.RegisterType<ISayHello, SayHelloInChinese>();
            ISayHello hello = container.Resolve<ISayHello>();
            hello.Run();
        }

首先,我在裡面加入了一行程式碼,呼叫容器物件的 RegistertType 方法來指定 ISayHello 這個介面所對應的具象型別是 SayHelloInChinese。望文生義,你也可以把這個動作稱為為「註冊型別」。

然後,在呼叫 Resolve 方法時,把原先傳入的具象型別 SayHelloInEnglish 改成了抽象型別 ISayHello 介面。由於先前已經註冊好 ISayHello 所對應的具象型別,因此 Unity 在解析物件時,會知道應該用 SayHelloInChinese 來建立物件,而不是 SayHelloInEnglish。

上例的執行結果為:



進一步來看前例中的 RegisterType 泛型方法。它其實是個擴充方法(extension method),定義在這個類別裡:Microsoft.Practices.Unity.UnityContainerExtensions。此方法所要擴充的目標是 IUnityContainer 介面,而且它有多種版本。前例使用的是這個版本:

IUnityContainer RegisterType<TFrom, TTo>(
 params InjectionMember[] injectionMembers
) where TTo: TFrom

從泛型參數 TFrom 和 TTo 的名稱可以約略看出,此方法的用途是告訴 DI 容器:在建立 TFrom 型別的物件實體時,請使用 TTo 型別。此方法可以傳入一個參數陣列,讓你設定額外的屬性(暫且略過,往後有機會再進一步說)。另外要注意的是,原型宣告有一個 where 子句,告訴 C# 編譯器:TTo 必須繼承或實作 TFrom。如此一來,當我們在應用程式中註冊型別對應時,萬一不小心指定了不相容的型別,在編譯時期就能發現。

註:C# 參數陣列可以讓你傳入不定個數的參數,當然也包括零個參數。因此,前面的範例程式碼並沒有傳遞任何參數給這個方法也能通過編譯。
這個泛型擴充方法 RegisterType 其實會轉而呼叫 IUnityContainer 介面的 RegisterType 弱型別(weakly typed)方法:

IUnityContainer RegisterType(
 Type from,
 Type to,
 params InjectionMember[] injectionMembers
)

之所以說「弱型別」,是因為它所傳入的參數 from(來源型別)和 to(目標型別) 都是基礎型別 Type。你如果直接呼叫此方法,而且對 fromto 分別傳入不相容的型別,編譯器會檢查不出來。例如:

        static void Main(string[] args)
        {
            var container = new UnityContainer();            
            container.RegisterType(typeof(ISayHello), typeof(string));
            ISayHello hello = container.Resolve<ISayHello>();
            hello.Run();
        }

這樣編譯會過,可是執行時會出現錯誤,因為 ISayHello 和 string 彼此並非相容型別。這就是為什麼前面的範例會使用泛型版本的 RegisterType 方法的緣故。如果沒有特殊原因(下一篇會有一個例子),一般建議是盡量使用強型別的版本,以獲得編譯時期型別檢查的好處。

註冊了抽象型別與具象型別的對應關係之後,並不是說以後就限定你只能使用抽象型別來建立物件。所以底下的程式片段中,後面兩行程式碼的作用是完全一樣的:

        container.RegisterType<ISayHello, SayHelloInChinese>();
        ISayHello hello1 = container.Resolve<ISayHello>();
        ISayHello hello2 = container.Resolve<SayHelloInChinese>();

最後,應用程式可能會需要註冊多組型別對應。這沒問題,只要呼叫多次 RegisterType 方法就行。例如:

        container.RegisterType<IBeer, TaiwanBeer>();
        container.RegisterType<IQoo, Qoo>();

這會將 IBeer 對應至 TaiwanBeer 類別,以及把 IQoo 對應至 Qoo 類別。

那麼,一個抽象型別可以對應至多個具象型別嗎?嗯,是可以這麼寫,例如:

        container.RegisterType<ISayHello, SayHelloInChinese>();
        container.RegisterType<ISayHello, SayHelloInEnglish>();

程式的確能夠順利編譯和執行。可是,實際建立物件時一定只用一個型別,那麼究竟是哪一個呢?

答案是 SayHelloInEnglish。

至於原因,這就得要提到 Unity 的「預設對應」規則了。這個部分就留到下次再談吧!
Happy coding!


參考資料:Dependency Injection in .NET by Mark Seemann

沒有留言:

技術提供:Blogger.
回頂端⬆️