天天看點

深入了解控制反轉和依賴注入

之前一直都不太了解這兩個概念,這段時間在學習ASP.NET CORE,而ASP.NET CORE是建立在依賴注入架構之上的,是以認真學習了一下。

所謂控制反轉,其實就是從主動變成被動。以前我們在應用程式中要new一個對象就是很直接地new,這樣做其實是違反了程式設計中最重要的一個原則:低耦合。

因為在A類中直接引用B類就造成A是直接依賴B的,這樣設計出來的程式可擴充性是很差的,這種方式我們稱之為主動。

那什麼是被動呢?被動就是A類如果需要引用B類,不是在A類中直接執行個體化B類,而是由架構來提供B類的執行個體。

這種情況下,一般B類是繼承自某個接口或者抽象類的,A類引用的是接口或者抽象類,這樣就符合面向接口程式設計的設計原則了,A和B就解耦了。

那說到這裡,大家肯定會問,那架構怎麼提供B類的執行個體呢?

其實有很多種方式,比如:模闆方法、工廠方法、抽象工廠。這些方法的理念其實都差不多:定義一個接口或抽象類C,讓B繼承C,在B中實作

那些抽象的方法或者虛方法,這個就不詳細介紹了。

依賴注入其實是實作控制反轉的一種架構,控制反轉是一種設計理念,而依賴注入是實作這個理念的一種架構,這也是為什麼他們兄弟倆經常被

一起提起的原因。

依賴注入通俗一點的解釋是:假如A依賴B,B繼承自接口或抽象類C,那A類中引用的應該是C,使A與B解耦。那注入怎麼解釋呢?其實就是在編寫代碼時

配置C的具體實作類,程式啟動後,依賴注入架構會把C和B的關系存儲在一個ServiceDescriptor類中,當A需要使用C時,依賴注入架構會根據B和C的映射關系執行個體化一個B給到A。

這樣看來,依賴注入這個名字起的還是挺貼切是吧。

那依賴注入架構具體是怎麼提供B類的執行個體呢?下面我們從純理論的角度來說明一下依賴注入架構的内部實作(最好是下載下傳ASP.NET CORE的源碼來看,這樣會了解的更加詳細)

依賴注入的方式有3中:構造器注入、屬性注入、方法注入。我們最常用的是構造器注入,當然在ASP.NET CORE的startup類的ConfigureServices和Configure函數中也使用了方法注入。

我們先講一下依賴注入的注冊過程吧。大家都知道我們通過調用IServiceCollection的Add方法來注冊服務(IServiceCollection有幾個擴充方法,可以添加不同生命周期的服務,但最終都是調用Add方法),

那這個架構具體是怎麼存儲這些注冊資訊的呢?如果不考慮細節的話,其實也挺簡單的。就是在ServiceCollection(IServiceCollection接口的預設實作)中定義了List<ServiceDescriptor>類型的全局變量,

一個注冊的服務的所有參數都定義在ServiceDescriptor類中,比如:服務類型(ServiceType)、實作類型(ImplementationType)、生命周期(Lifetime)等屬性。每添加一個服務注冊,這個List就添加一個ServiceDescriptor。

注冊說完了,我們再說一下服務的消費,就是架構如何執行個體化對應的執行個體。

我們先來說一下注冊的實作類需要滿足的一些條件吧。第一這個實作類必須要有一個公共的構造函數,不然怎麼執行個體化呢?第二構造函數如果有參數的話,那參數必須是

依賴注入架構必須能夠提供的,比如這個參數是已經注冊到架構的某個接口或者抽象類。

這裡引申出了2個問題,下面我們看一下代碼:

services.AddSingleton<IFoo, Foo>();
services.AddSingleton<IBar, Bar>();
services.AddSingleton<IQux, Qux>();
           
public class Qux:IQux
    {
        public Qux(IFoo foo)
        {
            Console.WriteLine($"Selected constructor:Qux(IFoo foo)");
        }
        public Qux(IFoo foo,IBar bar)
        {
            Console.WriteLine($"Selected constructor:Qux(IFoo foo,IBar bar)");
        }
    }
           

Qux有2個構造函數,那依賴注入架構會選擇哪一個呢?我們在把IQux放到某個控制器的構造函數中,運作起來看一下:

深入了解控制反轉和依賴注入

被選擇構造函數是第二個,這個例子說明構造函數的參數必須是依賴注入架構能夠提供的所有參數的子集。如果找不到比對的參數就會報錯。

我們再看一種情況:

public class Qux:IQux
    {
        public Qux(IFoo foo,IBar bar)
        {
            Console.WriteLine($"Selected constructor:Qux(IFoo foo,IBar bar)");
        }
        public Qux(IFoo foo,IBaz baz)
        {
            Console.WriteLine($"Selected constructor:Qux(IFoo foo,IBaz baz)");
        }
    }
           
services.AddSingleton<IFoo, Foo>();
services.AddSingleton<IBar, Bar>();
services.AddSingleton<IBaz, Baz>();
services.AddSingleton<IQux, Qux>();
           

這種情況下這2個構造函數都滿足,架構就無法選擇合适的構造函數,直接報錯了。

結合生命周期,我們再考慮深入一點,如果Qux的生命周期為Singleton,Foo的生命周期為Scope,而Qux引用了Foo,那會發生什麼呢?

services.AddScoped<IFoo, Foo>();          
services.AddSingleton<IQux, Qux>();
           
public class Qux:IQux
    {
        public Qux(IFoo foo)
        {
            Console.WriteLine($"Selected constructor:Qux(IFoo foo)");
        }
 
    }
           

運作結果為:

System.AggregateException:“Error while validating the service descriptor 'ServiceType: ValidateScope.IQux Lifetime: Singleton ImplementationType: ValidateScope.Qux': Cannot consume scoped service 'Vali”
           

意思就是生命周期為Singleton的類型Qux不能消費生命周期為Scope的Foo。這個其實也很好了解,Singleton服務是存儲在根容器的,是單例的,它的生命周期是從程式啟動開始到程式停止運作結束的,貫穿整個應用程式的生命周期。

而Scope服務是存儲在根容器下的子容器的,它的生命周期是從一個請求開始到結束的,如果Singleton服務引用了Scope服務,就相當于把Scope服務放到Singleton服務裡面去了,但是注冊的時候是注冊為Scope服務,是以這個肯定是

沖突的。

下面說一下服務執行個體的建立過程,一共有4中方式建立服務的執行個體:

1、RuntimeServiceProviderEngine,采用反射的方式提供服務執行個體

2、ILEmitServiceProviderEngine,采用IL Emit的方式提供服務執行個體

3、ExpressionsServiceProviderEngine,采用表達式樹的方式提供服務執行個體

4、DynamicServiceProviderEngine,根據請求并發數量動态決定最終的服務執行個體提供方案

架構預設采用的方式是第四種。其實我也隻懂反射的方式,其他的也沒怎麼了解,大家有興趣的話可以自己下載下傳ASP.NET CORE的源碼研究一下。

以上就是我對控制反轉和依賴注入的了解,如有說的不對的地方,請留言指正,謝謝!

繼續閱讀