[ASP.NET Core 3框架揭秘] 依賴注入[3]:依賴注入模式


IoC主要體現了這樣一種設計思想:通過將一組通用流程的控制權從應用轉移到框架之中以實現對流程的復用,並按照“好萊塢法則”實現應用程序的代碼與框架之間的交互。我們可以采用若干設計模式以不同的方式實現IoC,比如我們在前面介紹的模板方法、工廠方法和抽象工廠,接下來我們介紹一種更有價值的IoC模式:依賴注入(DI:Dependency Injection)。

一、由容器提供對象

和前面介紹的工廠方法和抽象工廠模式一樣,依賴注入是一種“對象提供型”的設計模式,在這里我們將提供的對象統稱為“服務”、“服務對象”或者“服務實例”。在一個采用依賴注入的應用中,我們定義某個類型的時候,只需要直接將它依賴的服務采用相應的方式注入進來就可以了。

在應用啟動的時候,我們會對所需的服務進行全局注冊。一般來說,服務大都是針對實現的接口或者繼承的抽象類進行注冊的,服務注冊信息的幫助我們在后續消費過程中提供對應的服務實例。按照“好萊塢法則”,應用只需要定義並注冊好所需的服務,服務實例的提供則完全交給框架來完成,框架則會利用一個獨立的“容器(Container)”來提供所需的每一個服務實例。

我們將這個被框架用來提供服務的容器稱為“依賴注入容器”,也有很多人將其稱為“IoC容器”,根據前面針對IoC的介紹,我不認為后者是一個合理的稱謂。依賴注入容器之所以能夠按照我們希望的方式來提供所需的服務是因為該容器是根據服務注冊信息來創建的,服務注冊了包含提供所需服務實例的所有信息。

舉個簡單的例子,我們創建一個名為Cat的依賴注入容器類型,那么我們可以調用如下這個擴展方法GetService<T>從某個Cat對象中獲取指定類型的服務對象。我之所以將其命名為Cat,源於我們大家都非常熟悉的一個卡通形象“機器貓(哆啦A夢)”。機器貓的那個四次元口袋就是一個理想的依賴注入容器,大熊只需要告訴哆啦A夢相應的需求,它就能從這個口袋中得到相應的法寶。依賴注入容器亦是如此,服務消費者只需要告訴容器所需服務的類型(一般是一個服務接口或者抽象服務類),就能得到與之匹配的服務實例。

public static class CatExtensions
{  
    public static T GetService<T>(this Cat cat);
}

對於我們演示的MVC框架來說,我們在前面分別采用不同的設計模式對框架的核心類型MvcEngine進行了“改造”,現在我們采用依賴注入的方式,並利用上述的這個Cat容器按照如下的方式對其進行重新實現,我們會發現MvcEngine變得異常簡潔而清晰。

public class MvcEngine
{
    public Cat Cat { get; }
    public MvcEngine(Cat cat) => Cat = cat;
        
    public async Task StartAsync(Uri address)
    {
        var listener = Cat.GetService<IWebListener>();
        var activator = Cat.GetService<IControllerActivator>();
        var executor = Cat.GetService<IControllerExecutor>();
        var renderer = Cat.GetService<IViewRenderer>();

        await listener.ListenAsync(address);
        while (true)
        {
            var httpContext = await listener.ReceiveAsync();
            var controller = await activator.CreateControllerAsync(httpContext);
            try
            {
                var view = await executor.ExecuteAsync(controller, httpContext);
                await renderer.RenderAsync(view, httpContext);
            }
            finally
            {
                await activator.ReleaseAsync(controller);
            }
        }
    }        
}

依賴注入體現了一種最為直接的服務消費方式,消費者只需要告訴提供者(依賴注入容器)所需服務的類型,后者就能根據預先注冊的規則提供一個匹配的服務實例。由於服務注冊最終決定了依賴注入容器根據指定的服務類型會提供一個怎樣的服務實例,所以我們可以通過修改服務注冊的方式來實現對框架的定制。如果應用程序需要采用前面定義的SingletonControllerActivator以單例的模式來激活目標Controller,那么它可以在啟動MvcEngine之前按照如下的形式將SingletonControllerActivator注冊到依賴注入容器上就可以了。

public class App
{
    static void Main(string[] args)
    {
        var cat = new Cat() .Register<ControllerActivator, SingletonControllerActivator>();
        var engine     = new MvcEngine(cat);
        var address     = new Uri("http://localhost/mvcapp");
        engine.StartAsync(address);
    }
}

二、三種依賴注入方式

一項任務往往需要多個對象相互協作才能完成,或者說某個對象在完成某項任務的時候需要直接或者間接地依賴其他的對象來完成某些必要的步驟,所以運行時對象之間的依賴關系是由目標任務來決定的,是“恆定不變的”,自然也無所謂“解耦”的說法。但是運行時對象通過對應的類來定義,類與類之間耦合則可以通過對依賴進行抽象的方式來降低或者解除。

從服務消費的角度來講,我們借助於一個接口對消費的服務進行抽象,那么服務消費程序針對具體服務類型的依賴可以轉移到對服務接口的依賴上面,但是在運行時提供給消費者的總是一個針對某個具體服務類型的對象。不僅如此,要完成定義在服務接口的操作,這個對象可能需要其他相關對象的參與,換句話說,提供的這個依賴服務對象可能具有對其他服務對象的依賴。作為服務對象提供者的依賴注入容器,它會根據這一依賴鏈提供所有的依賴服務實例。

如下圖所示,應用框架調用GetService<IFoo>方法向依賴注入容器索取一個實現了IFoo接口的服務對象,后者會根據預先注冊的類型映射關系創建一個類型為Foo的對象。由於Foo對象需要Bar和Gux對象的參與才能完成目標操作,所以Foo具有了針對Bar和Gux的直接依賴。至於服務對象Bar,它又依賴Baz,那么Baz成為了Foo的間接依賴。對於依賴注入容器最終提供的Foo對象,它所直接或者間接依賴的對象Bar、Baz和Qux都會預先被初始化並自動注入到該對象之中。

3-5

從面向對象編程的角度來講,類型中的字段或者屬性是依賴的一種主要體現形式。如果類型A中具有一個B類型的字段或者屬性,那么A就對B產生了依賴,所以我們可以將依賴注入簡單地理解為一種針對依賴字段或者屬性的自動化初始化方式。我們可以通過三種主要的方式達到這個目的,這就是接下來着重介紹的三種依賴注入方式。

構造器注入

構造器注入就是在構造函數中借助參數將依賴的對象注入到由它創建的對象之中。如下面的代碼片段所示,Foo針對Bar的依賴體現在只讀屬性Bar上,針對該屬性的初始化實現在構造函數中,具體的屬性值由構造函數傳入的參數提供。

public class Foo
{
    public IBar Bar{get;}
    public Foo(IBar bar) =>Bar = bar;
}

除此之外,構造器注入還體現在對構造函數的選擇上。如下面的代碼片段所示,Foo類定義了兩個構造函數,依賴注入容器在創建Foo對象之前首先需要選擇一個適合的構造函數。至於目標構造函數如何選擇,不同的依賴注入容器可能有不同的策略,比如可以選擇參數最多或者最少的構造函數,或者可以按照如下所示的方式在目標構造函數上標注一個InjectionAttribute特性。

public class Foo
{
    public IBar Bar{get;}
    public IBaz Baz {get;}

    [Injection]
    public Foo(IBar bar) =>Bar = bar;
    public Foo(IBar bar, IBaz):this(bar)=>Baz = baz;
}

屬性注入

如果依賴直接體現為類的某個屬性,並且該屬性不是只讀的,我們可以讓依賴注入容器在對象創建之后自動對其進行賦值進而達到依賴注入的目的。一般來說,我們在定義這種類型的時候,需要顯式將這樣的屬性標識為需要自動注入的依賴屬性以區別於其他普通的屬性。如下面的代碼片段所示,Foo類中定義了兩個可讀寫的公共屬性Bar和Baz,我們通過標注InjectionAttribute特性的方式將屬性Baz設置為自動注入的依賴屬性。對於由依賴注入容器提供的Foo對象,它的Baz屬性將會自動被初始化。

public class Foo
{
    public IBar Bar{get; set;}

    [Injection]
    public IBaz Baz {get; set;}
}

方法注入

體現依賴關系的字段或者屬性可以通過方法的形式初始化。如下面的代碼片段所示,Foo針對Bar的依賴體現在只讀屬性上,針對該屬性的初始化實現在Initialize方法中,具體的屬性值由該方法的傳入的參數提供。我們同樣通過標注特性(InjectionAttribute)的方式將該方法標識為注入方法。依賴注入容器在調用構造函數創建一個Foo對象之后,它會自動調用這個Initialize方法對只讀屬性Bar進行賦值。

public class Foo
{
    public IBar Bar{get;}

    [Injection]
    public Initialize(IBar bar)=> Bar = bar;
}

除了上述這種通過依賴注入容器在初始化服務過程中自動調用的實現之外,我們還可以利用它實現另一種更加自由的方法注入,這種注入方式在ASP.NET Core應用中具有廣泛的應用。ASP.NET Core在啟動的時候會調用注冊的Startup對象來完成中間件的注冊,我們定義這個Startup類型的時候不需要讓它實現某個接口,所以用於注冊中間件的Configure方法沒有一個固定的聲明,我們可以按照如下的方式將任意依賴的服務實例直接注入到這個方法中。

public class Startup
{
    public void Configure(IApplicationBuilder app, IFoo foo, IBar bar, IBaz baz);
}

類似的注入方式同樣應用到中間件類型的定義上。與用來注冊中間件的Startup類型一樣,ASP.NET Core框架下的中間件類型同樣不需要實現某個預定義的接口,用於處理請求的InvokeAsync或者Invoke方法同樣可以按照如下的方式注入任意的依賴服務。

public class FoobarMiddleware
{
    private readonly RequestDelegate _next; 
    public FoobarMiddleware(RequestDelegate next)=> _next = next;

    public Task InvokeAsync(HttpContext httpContext, IFoo foo, IBar bar, IBaz baz);
}

上面這種方式的方法注入促成了一種“面向約定”的編程方式。由於不再需要實現某個預定義的接口或者繼承某一個預定義的基類,需要實現或者重寫方法的聲明也就少了對應的限制,這樣就可以采用最直接的方式將依賴的服務注入到方法中。對於前面介紹的這幾種注入方式,構造器注入是最為理想的形式,我個人不建議使用屬性注入和方法注入(前面介紹的這種基於約定的方法注入除外)。

三、Service Locator模式

假設我們需要定義一個服務類型Foo,它依賴於另外兩個服務Bar和Baz,后者對應的服務接口分別為IBar和IBaz。如果當前應用中具有一個依賴注入容器(假設類似於我們在前面定義的Cat),那么我們可以采用如下兩種方式來定義這個服務類型Foo。

public class Foo : IFoo
{
    public IBar Bar { get; }
    public IBaz Baz { get; }
    public Foo(IBar bar, IBaz baz)
    {
        Bar = bar;
        Baz = baz;
    }  
    public async Task InvokeAsync()
    {
        await Bar.InvokeAsync();
        await Baz.InvokeAsync();
    }
}

public class Foo : IFoo
{
    public Cat Cat { get; }
    public Foo(Cat cat) => Cat = cat; 
    public async Task InvokeAsync()
    {
        await Cat.GetService<IBar>().InvokeAsync();
        await Cat.GetService<IBaz>().InvokeAsync();
    }
}

從表面上看,上面提供的這兩種服務類型的定義方式貌似都不錯,至少它們都解決針對依賴服務的耦合問題,並將針對服務實現的依賴轉變成針對接口的依賴。那么哪一種更好呢?我想有人會選擇第二種定義方式,因為這種定義方式不僅僅代碼量更少,針對服務的提供也更加直接。我們直接在構造函數中“注入”了代表“依賴注入容器”的Cat對象,在任何使用到依賴服務的地方,我們只需要利用它來提供對應的服務實例就可以了。

但事實上第二種定義方式采用的設計模式根本就不是“依賴注入”,而是一種被稱為“Service Locator”的設計模式。Service Locator模式同樣具有一個通過服務注冊創建的全局的容器來提供所需的服務實例,該容器被稱為“Service Locator”。“依賴注入容器”和“Service Locator”實際上是同一事物在不同設計模式中的不同稱謂罷了,那么依賴注入和Service Locator之間的差異體現在什么地方呢?

我覺得可以從“依賴注入容器”或者“Service Locator”被誰使用的角度來區分這兩種設計模式的差別。在一個采用依賴注入的應用中,我們只需要采用標准的注入形式將服務類型定義好,並在應用啟動之前完成相應的服務注冊就可以了,框架自身的引擎在運行過程中會利用依賴注入容器來提供當前所需的服務實例。換句話說,依賴注入容器的使用者應該是框架而不是應用程序。Service Locator模式顯然不是這樣,很明顯是應用程序在利用它來提供所需的服務實例,所以它的使用者是應用程序

我們也可以從另外一個角度區分兩者之間的差別。由於依賴服務是以“注入”的方式來提供的,所以采用依賴注入模式的應用可以看成是將服務“推”給依賴注入容器,Service Locator模式下的應用則是利用Service Locator去“”取所需的服務,這一推一拉也准確地體現了兩者之間的差異。那么既然兩者之間有差別,究竟孰優孰劣呢?

早在2010年,Mark Seemann就在他的博客中將Service Locator視為一種“反模式(Anti-Pattern)”,雖然也有人對此提出不同的意見,但我個人是非常不推薦使用這種設計模式的。我反對使用Service Locator與前面提到的反對使用屬性注入和方法注入具有類似的緣由。

本着“松耦合、高內聚”的設計原則,我們既然將一組相關的操作定義在一個能夠復用的服務中,就應該盡量要求服務自身不但具有獨立和自治的特性,也要求服務之間的應該具有明確的界限,服務之間的依賴關系應該是明確的而不是模糊的。不論是采用屬性注入或者方法注入,還是使用Service Locator來提供當前依賴的服務,這無疑為當前的服務增添了一個新的依賴,即針對依賴注入容器或者Service Locator的依賴。

當前服務針對另一個服務的依賴與針對依賴注入容器或者Service Locator的依賴具有本質的不同,前者是一種基於類型的依賴,不論是基於服務的接口還是實現類型,這是一種基於“契約”的依賴。這種依賴不僅是明確的,也是有保障的。但是依賴注入容器或者Service Locator本質上是一個黑盒,它能夠提供所需服務的前提是相應的服務注冊已經預先添加了容器之中,但是這種依賴不僅是模糊的也是不可靠的。

ASP.NET Core框架使用的依賴注入框架只支持構造器注入,而不支持屬性和方法注入(類似於Startup和中間件基於約定的方法注入除外),但是我們很有可能不知不覺地會按照Service Locator模式來編寫我們的代碼。從某種意義上講,當我們在程序中使用IServiceProvider(表示依賴注入容器)來提取某個服務實例的時候,就意味着我們已經在使用Service Locator模式了,所以當我們遇到這種情況下的時候應該多想一想是否一定需要這么做。

[ASP.NET Core 3框架揭秘] 依賴注入[1]:控制反轉
[ASP.NET Core 3框架揭秘] 依賴注入[2]:IoC模式
[ASP.NET Core 3框架揭秘] 依賴注入[3]:依賴注入模式
[ASP.NET Core 3框架揭秘] 依賴注入[4]:一個迷你版DI框架
[ASP.NET Core 3框架揭秘] 依賴注入[5]:利用容器提供服務
[ASP.NET Core 3框架揭秘] 依賴注入[6]:服務注冊
[ASP.NET Core 3框架揭秘] 依賴注入[7]:服務消費
[ASP.NET Core 3框架揭秘] 依賴注入[8]:服務實例的生命周期
[ASP.NET Core 3框架揭秘] 依賴注入[9]:實現概述
[ASP.NET Core 3框架揭秘] 依賴注入[10]:與第三方依賴注入框架的適配


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM