1. 程式人生 > >依賴注入[4]: 建立一個簡易版的DI框架[上篇]

依賴注入[4]: 建立一個簡易版的DI框架[上篇]

本系列文章旨在剖析.NET Core的依賴注入框架的實現原理,到目前為止我們通過三篇文章(《控制反轉》、《基於IoC的設計模式》和《 依賴注入模式》)從純理論的角度對依賴注入進行了深入論述,為了讓讀者朋友能夠更好地理解.NET Core的依賴注入框架的設計思想和實現原理,我們建立了一個簡易版本的DI框架,也就是我們在前面文章中多次提及的Cat。我們會上下兩篇來介紹這個被稱為為Cat的DI框架,上篇介紹程式設計模型,下篇關注設計實現。[原始碼從這裡下載]

目錄
一、DI容器的層次結構與服務例項生命週期
二、服務的註冊於提取
三、提供泛型服務
四、多服務例項的提取
五、服務例項的釋放回收

一、DI容器的層次結構與服務例項生命週期

雖然我們對這個名為Cat的DI框架進行了最大限度的簡化,但是與.NET Core的真實DI框架相比,Cat不僅採用了一致的設計,而且幾乎具備了後者所有的功能特性。作為DI容器的Cat物件不僅僅是作為服務例項的提供者,它同時還需要維護提供服務例項的生命週期。Cat提供了三種生命週期模式,如果要了解它們之間的差異,就必需對多個Cat之間的層次關係有充分的認識。一個代表DI容器的Cat用以來建立多個新的Cat物件,後者視前者為“父容器”,所以多個Cat物件通過其“父子關係”維繫一個樹形層次化結構。不過著僅僅是一個邏輯結構而已,實際上每個Cat物件只會按照圖1所示的方式引用整棵樹的

3-7
圖1 Cat之間的關係

在瞭解了代表DI容器的多個Cat物件之間的關係之後,對於三種預定義的生命週期模式就很好理解了。如下所示的Lifetime列舉代表著三種生命週期模式,其中Transient代表容器針對每次服務請求都會建立一個新的服務例項,它代表一種“即用即取,用完即棄”的消費方式;而Self則是將提供服務例項儲存在當前容器中,它代表針對某個容器的單例模式; Root則是將每個容器提供的服務例項統一存放到根容器中,所以該模式能夠在多個“同根”容器範圍內確保提供的服務是單例的。

public enum Lifetime
{
    Root,
    Self,
    Transient
}
代表DI容器的Cat物件為我們提供所需服務例項的前提是相應的服務註冊已經在此之前已經新增到容器之中。服務總是針對服務型別(介面、抽象類或者具體型別)來註冊的,Cat通過定義的擴充套件方法提供瞭如下三種註冊方式。除了以指定服務例項的形式外(預設採用Root模式),我們在註冊服務的時候必須指定一個具體的生命週期模式。
  • 指定註冊服務的實現型別;
  • 指定一個現有的服務例項;
  • 指定一個建立服務例項的工廠。

二、服務的註冊於提取

我們定義瞭如下的介面和對應的實現型別來演示針對Cat的服務註冊和提取。其中Foo、Bar和Baz分別實現了對應的介面IFoo、IBar和IBaz,為了反映Cat對服務例項生命週期的控制,我們讓它們派生於同一個基類Base。Base實現了IDisposable介面,我們在其建構函式和實現的Dispose方法中打印出相應的文字以確定對應的例項何時被建立和釋放。我們還定義了一個泛型的介面IFoobar<T1, T2>和對應的實現類Foobar<T1, T2>來演示Cat針對泛型服務例項的提供。

public interface IFoo {}
public interface IBar {}
public interface IBaz {}
public interface IFoobar<T1, T2> {}
public class Base : IDisposable
{
    public Base() => Console.WriteLine($"An instance of {GetType().Name} is created.");
    public void Dispose() => Console.WriteLine($"The instance of {GetType().Name} is disposed.");
}

public class Foo : Base, IFoo, IDisposable { }
public class Bar : Base, IBar, IDisposable { }
public class Baz : Base, IBaz, IDisposable { }
public class Foobar<T1, T2>: IFoobar<T1,T2>
{
    public IFoo Foo { get; }
    public IBar Bar { get; }
    public Foobar(IFoo foo, IBar bar)
    {
        Foo = foo;
        Bar = bar;
    }
}

在如下所示的程式碼片段中我們建立了一個Cat物件並採用上面提到的方式針對介面IFoo、IBar和IBaz註冊了對應的服務,它們採用的生命週期模式分別為Transient、Self和Root。接下來我們利用Cat物件建立了它的兩個子容器,並利用呼叫後者的GetService<T>方法來提供相應的服務例項。

class Program
{
    static void Main()
    {
        var root = new Cat()
            .Register<IFoo, Foo>(Lifetime.Transient)
            .Register<IBar>(_=> new Bar(), Lifetime.Self) 
            .Register<IBaz, Baz>( Lifetime.Root);
        var cat1 = root.CreateChild();
        var cat2 = root.CreateChild();

        void GetServices<TService>(Cat cat)
        {
            cat.GetService<TService>();
            cat.GetService<TService>();
        }

        GetServices<IFoo>(cat1);
        GetServices<IBar>(cat1);
        GetServices<IBaz>(cat1);
        Console.WriteLine();
        GetServices<IFoo>(cat2);
        GetServices<IBar>(cat2);
        GetServices<IBaz>(cat2);
    }
}
上面的程式執行之後會在控制檯上輸出如圖2所示的結果,輸出的內容不僅表明Cat能夠根據新增的服務註冊提供對應型別的服務例項,還體現了它對生命週期的控制。由於IFoo被註冊為Transient服務,所以Cat針對該介面型別的四次請求都會建立一個全新的Foo物件。IBar服務的生命週期模式為Self,如果我們利用同一個Cat物件來提供對應的服務例項,該Cat物件只會建立一個Bar物件,所以整個程式執行過程中會建立兩個Bar物件。IBaz服務採用Root生命週期,所以具有同根的兩個Cat物件提供的總是同一個Baz物件,後者只會被建立一次。

3-8
圖2 Cat按照服務註冊對應的生命週期模式提供服務例項

三、提供泛型服務

除了提供類似於IFoo、IBar和IBaz這樣非泛型服務例項之外,如果具有對應的泛型定義(Generic Definition)的服務註冊,Cat同樣也能提供泛型服務例項。如下面的程式碼片段所示,在為建立的Cat物件添加了針對IFoo和IBar介面的服務註冊之後,我們呼叫Register方法註冊了針對泛型定義IFoobar<,>的服務註冊,實現的型別為Foobar<,>。當我們利用該Cat物件提供一個型別為IFoobar<IFoo, IBar>的服務例項的時候,它會建立並返回一個Foobar<Foo, Bar>物件。

var cat = new Cat()
    .Register<IFoo, Foo>(Lifetime.Transient)
    .Register<IBar, Bar>(Lifetime.Transient)
    .Register(typeof(IFoobar<,>), typeof(Foobar<,>), Lifetime.Transient);

var foobar = (Foobar<IFoo, IBar>)cat.GetService<IFoobar<IFoo, IBar>>();
Debug.Assert(foobar.Foo is Foo);
Debug.Assert(foobar.Bar is Bar);

四、多服務例項的提取

當我們在進行服務註冊的時候,可以為同一個型別新增多個服務註冊。不過由於擴充套件方法GetService<T>總是返回一個唯一的服務例項,我們對該方法採用了“後來居上”的策略,即總是採用最近新增的服務註冊來建立服務例項。如果我們呼叫另一個擴充套件方法GetServices<T>,它將利用返回所有服務註冊提供的服務例項。

如下面的程式碼片段所示,我們為建立的Cat物件添加了三個針對Base型別的服務註冊,對應的實現型別分別為Foo、Bar和Baz。我們最後將Base作為泛型引數呼叫了GetServices<Base>方法,該方法會返回包含三個Base物件的集合,集合元素的型別分別為Foo、Bar和Baz。

var services = new Cat()
    .Register<Base, Foo>(Lifetime.Transient)
    .Register<Base, Bar>(Lifetime.Transient)
    .Register<Base, Baz>(Lifetime.Transient)
    .GetServices<Base>();
Debug.Assert(services.OfType<Foo>().Any());
Debug.Assert(services.OfType<Bar>().Any());
Debug.Assert(services.OfType<Baz>().Any());

五、服務例項的釋放回收

如果提供的服務例項實現了IDisposable介面,我們應該適當的時候呼叫其Dispose方法釋放該服務例項。由於服務例項的生命週期完全由作為DI容器的Cat物件來管理,通過呼叫Dispose方法來釋放服務例項自然也應該由它來負責。Cat針對提供服務例項的釋放策略取決於對應的服務註冊採用的生命週期模式,具體的策略如下:

  • Transient和Self:所有實現了IDisposable介面的服務例項會被作為服務提供者的當前Cat物件儲存起來,當Cat物件自身的Dispose方法被呼叫的時候,這些服務例項的Dispose方法會隨之被呼叫。

  • Root:由於服務例項儲存在作為根容器的Cat物件上,所以後者的Dispose方法的呼叫會觸發針對服務例項的釋放。

上述的釋放策略可以通過如下的演示例項來印證。我們在如下的程式碼片段中建立了一個Cat物件,並添加了針對IFoo、IBar和IBaz的服務註冊。接下來我們呼叫了CreateChild方法建立程式碼子容器的Cat物件,並用後者提供了三個註冊服務對應的例項。

class Program
{
    static void Main()
    {
        using (var root = new Cat()
            .Register<IFoo, Foo>(Lifetime.Transient)
            .Register<IBar, Bar>(Lifetime.Self)
            .Register<IBaz, Baz>(Lifetime.Root))
        {
            using (var cat = root.CreateChild())
            {
                cat.GetService<IFoo>();
                cat.GetService<IBar>();
                cat.GetService<IBaz>();
                Console.WriteLine("Child cat is disposed.");
            }
            Console.WriteLine("Root cat is disposed.");
        }
    }
}
由於兩個Cat物件的建立都是在using塊中進行的,所有針對它們的Dispose方法都會在using塊結束的地方被呼叫,為了確定方法被呼叫的時機,我們特意在控制檯上列印了相應的文字。該程式執行之後會在控制檯上輸出如圖3所示的結果,我們可以看到當作為子容器的Cat物件的Dispose方法被呼叫的時候,由它提供的兩個生命週期模式分別為Transient和Self的兩個服務例項(Foo和Bar)被正常釋放了。至於生命週期模式為Root的服務例項Baz,它的Dispose方法會延遲到作為根容器Cat物件被釋放的時候。

3-9
圖3 Root服務例項的釋放