接續上一篇,這次介紹 Unity 的 auto-wiring 功能。我把它翻譯成「自動匹配」。

本文大綱:
  • 小引
  • 自動匹配規則
  • 手動匹配
  • 循環參考問題

小引

採用 Constructor Injection 的方式來注入物件時,如果欲解析的類別僅提供一個建構函式,這種情形通常不會產生任何疑慮,因為 Unity 在進行解析時一定是呼叫那個唯一的建構函式來生成物件。然而,如果欲解析的類別提供了數個多載(overloaded)建構函式,Unity 就會使用其內建的一套規則來挑選建構函式。身為開發人員,我們有必要了解這套規則,一方面有助於正確解讀程式碼的運作邏輯,一方面則可避免因為不解其中規則而寫出了產生執行時期錯誤的程式碼。

以先前的 EmailService 為例,如果為這個類別增加一個帶參數的建構函式,使之具有兩個建構函式:

    public class EmailService : IMessageService
    {
        public EmailService()
        {
            Console.WriteLine("EmailMessageService ctor()");
        }

        public EmailService(string smtpHost)
        {
            Console.WriteLine("EmailMessageService ctor(smtpHost)");
        }
    }


那麼,當我們以下列程式碼來解析 EmailService 時,Unity 會拋出 ResolutionFailedException 錯誤。

    var container = new UnityContainer();
    var svc = container.Resolve();


這是因為預設情況下,Unity 會使用參數最多的那個建構函式,而非不帶任何參數的預設建構函式。於是,在解析 EmailService 時,Unity 容器會呼叫帶有 smtpHost 參數的建構函式,並試圖解析這個參數。在沒有提供額外設定的情況下,Unity 無法解析 string 型別的參數,於是拋出異常。

自動匹配規則

承上所述,這種根據特定規則來自動挑選建構函式的機制叫做自動匹配(auto-wiring)。底下是完整的自動匹配邏輯:

  1. 當目標型別具有多個建構函式,則選擇有套用 InjectionConstructor 特徵項(attribute)的那個建構函式;
  2. 如果全部的建構函式皆未套用 InjectionConstructor 特徵項,則使用參數個數最多的那個建構函式;
  3. 如果參數個數最多的建構函式並不只一個(例如有兩個建構函式都需要傳入 5 個參數),Unity 會拋出異常。
  4. 如果同一類別有兩個或兩個以上的建構函式套用了 InjectionConstructor 特徵項,Unity 也會拋異常。

了解這些規則之後,先前的問題便迎刃而解。例如,我們可以為預設建構函式套上 [InjectionConstructor],像這樣:

    public class EmailService : IMessageService
    {
        [InjectionConstructor]
        public EmailService()
        {
            Console.WriteLine("EmailMessageService ctor()");
        }

        public EmailService(string smtpHost)
        {
            Console.WriteLine("EmailMessageService ctor(smtpHost)");
        }
    }

如此一來,Unity 在進行解析 EmailService 時便會改用它的預設建構函式了。

手動匹配

除了剛才介紹的 InjectionConstructor 特徵項,Unity 也支援手動匹配的方式,以應付各種需求。關鍵的類別名稱也叫做 InjectionConstructor,它可以用於 Constructor Injection 的場合,讓我們在註冊類別時就明確指定要使用哪一個建構函式,並且連同目標建構函式所需要的傳入參數也都預先準備好。
注意:如果同時使用了 InjectionConstructor 類別和 InjectionConstructor 特徵項,Unity 會採用前者。
沿用先前範例的 NotificationManager 類別。現在假設要讓它支援多種訊息通知機制,於是寫了三個建構函式,分別可傳入零個、一個、和兩個 IMessageService 物件。如下所示:

    class NotificationManager
    {
        public NotificationManager()
        {
            Console.WriteLine("呼叫了無參數的建構函式。");
        }

        public NotificationManager(IMessageService svc)
        {
            Console.WriteLine("呼叫了一個參數的建構函式。");
        }

        public NotificationManager(IMessageService svc1, IMessageService svc2)
        {
            Console.WriteLine("呼叫了兩個參數的建構函式。");
        }
    }   

底下是註冊與解析的程式碼:

        var container = new UnityContainer();

        // (1) 具名註冊 EmailService 和 SmsService 類別。
        container.RegisterType("email");
        container.RegisterType("sms");

        // (2) 註冊 NotificationManager 類別。
        container.RegisterType(
            new InjectionConstructor(
                new ResolvedParameter("email"),
                new ResolvedParameter("sms")
            )
        );

        // 解析 NotificationManager。
        container.Resolve();

說明:
  1. 註冊 EmailService 與 SmsService 採用具名註冊。註冊名稱分別是 "email" 和 "sms"。
  2. 註冊 NotificationManager 類別時傳入一個 InjectionConstructor 物件,此物件帶有兩個已解析的參數(ResolvedParameter)。於是,Unity 在解析 NotificationManager 時,便知道要呼叫帶有兩個 IMessageService 參數的建構函式。同樣地,如果想要使用預設建構函式,則只要寫成 new InjectionConstructor() 即可。

其中的 ResolvedParameter 類別是用來包裝「需要進一步解析的參數」。請注意,在 new 一個 ResolvedParamter 物件的時候並沒有立刻執行解析型別的動作,而是會等到將來呼叫 Resolve 方法時才會解析參數。

循環參考問題

如果兩個類別的建構函式都需要注入對方,像這樣:

        public class Foo : IFoo
        {
            public Foo()
            { }

            public Foo(IBar bar)  // Foo 需要注入 Bar 
            { }
        }

        public class Bar : IBar
        {
            public Bar(IFoo foo)  // Bar 又需要 Foo 
            { }
        }

那麼當 Unity 在解析 IFoo 或 IBar 時,便會因為循環參考而引發 StackOverflowException。原因即在於 Unity 的自動匹配(auto-wiring)機制預設會使用最多參數的那個建構函式。碰到這種情形,可在註冊型別時使用 InjectionConstructor 來指定其他建構函式。參考以下範例:

        // 註冊時一併指定將來解析元件時應使用不帶任何參數的建構函式。
        container.RegisterType(new InjectionConstructor());


摘自:《.NET 相依性注入》