Unity 入門 (2)

上一回的範例當中,我們不是直接用 new 運算子去建立 SayHelloInEnglish 的物件實體(instance),而是透過 Unity 容器的 Resolve 方法來幫我們完成這項工作。這次要進一步說明這個 Resolve 方法在建立物件時的一些細節。

方便閱讀起見,這裡再列出關鍵程式碼:

    var container = new UnityContainer();
    ISayHello hello = container.Resolve<SayHelloInEnglish>();

用法很簡單,就只是先建立一個 UnityContainer 物件,然後再呼叫該容器物件的 Resolve 方法。

這裡使用的 Resolve 方法是個泛型方法,它接受一個型別參數,用來指定欲建立之物件的型別。由於傳入的型別是 SayHelloInEnglis,Unity 容器就知道要用這個型別來建立物件實體。換言之,Unity 會去呼叫 SayHelloInEnglish 的建構子。

在此範例中,我們並沒有為 SayHelloInEnglish 撰寫任何建構子,所以 Unity 容器會自動使用 C# 編譯器幫我們產生的預設建構子來建立物件實體。

註:預設建構子就是不帶任何參數的建構子。當你的類別沒有提供任何建構子,C# 編譯器會自動幫你產生一個。如果有在類別中定義任何建構子,則 C# 編譯器不會幫你產生預設建構子。
那麼,在透過 Unity 容器的 Resolve 方法來建立物件時,若該物件的類別有提供一個或多個帶參數的建構子呢?Unity 會選擇使用哪一個版本的建構子來建立物件?

答案是:預設情況下,Unity 會選擇使用參數最多的那個建構子來建立物件。

進一步思考剛才那句話,也許你會有個疑問:「Unity 在幫我們建立物件時,傳遞給建構子的各個參數也必須要先建立實體,那麼在建立這些參數的物件時,又是使用哪個建構子呢?」

還是同樣規則,亦即使用參數個數最多的那個建構子。

比如說,把先前的範例改一下,為 SayHelloInEnglish 類別提供兩個建構子:

    class SayHelloInEnglish : ISayHello
    {
        public SayHelloInEnglish()
        {
        }
 
        public SayHelloInEnglish(User aUser)
        {
            Console.WriteLine("SayHelloInEnglish(User aUser) is called");
        }
 
        public void Run()
        {
            Console.WriteLine("Hello, Unity!");
        }
    }

按照先前描述的規則,當 Unity 在建立 SayHelloInEnglish 的物件時,會使用帶最多參數的建構子,也就是需要傳入一個 User 物件的那個建構子。假設 User 類別的建構子也有兩個:

    class User
    {
        public User()
        {
            Console.WriteLine("無名氏");
        }
 
        public User(string name)
        {
            Console.WriteLine(name);
        }
    }

那麼當 Unity 在使用 SayHelloInEnglish(User aUser) 這個版本建構子時,又需要先建立 User 物件,而 User 類別的建構子有兩種口味,於是選擇參數最多的那個,也就是User(string name)。問題是,要再建立參數 name 的物件實體時,Unity 並不是選擇 String 的預設建構子,而一樣是用最多參數的建構子:

String(
    sbyte* value,
    int startIndex,
    int length,
    Encoding enc
)

如此一路下去,Unity 最終會碰到無法建立某一層建構子所需的參數物件而拋出例外(exception)。於是程式執行時便會看到如下錯誤訊息:

Unhandled Exception: Microsoft.Practices.Unity.ResolutionFailedException: Resolu
tion of the dependency failed, type = "UnityDemo01.SayHelloInEnglish", name = "(
none)".
Exception occurred while: while resolving.
Exception is: InvalidOperationException - The type String cannot be constructed.
You must configure the container to supply this value.

前面描述的,都是在預設情況下所產生的行為,而此預設行為是可以改的。以剛才的範例來說,如果你希望 Unity 在建立 User 物件時使用預設建構子(不帶任何參數的建構子),就可以為那個建構子套用 InjectionConstructorAttribute。如下所示:

    class User
    {
        [InjectionConstructor]
        public User()
        {
            Console.WriteLine("無名氏");
        }
 
        public User(string name)
        {
            Console.WriteLine(name);
        }
    }

如此一來,Unity 在建立物件時就會去用你特別指定的建構子,程式也就能順利執行,不會再發生先前的錯誤了。執行結果如下圖:


小結

到目前為止,我們已經知道如何透過 Unity 容器的 Resolve 方法來幫我們建立物件,也知道在建立物件時,Unity 預設會使用最多參數的建構子,而我們又如何利用 InjectionConstructorAttribute 來改變此預設行為。

可是,在呼叫 Resolve 方法時,我們傳入的型別是 SayHelloInEnglish,而不是 ISayHello 介面。這種寫法不是挺好,因為這會讓我們的程式跟 SayHelloInEnglish 類別綁得太緊。而且,從 Resolve 方法的名稱應該就看得出來,這個方法並不單單只是幫我們建立類別的 instance,它其實還有「解析型別」的功能。

如果你看過先前的 Dependency Injection 筆記,也許還記得裡面有提到的一個原則:要針對介面,而非針對實作類別來寫程式。所以,在下一篇文章裡,我會繼續修改這個範例程式,朝向「針對介面寫程式」的目標前進,並展示 Resolve 方法的主要功能:解析物件型別。

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

續集:Unity 入門 (3)

沒有留言:

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