IOC(控制翻轉)是程序設計的一種思想,其本質就是上端對象不能直接依賴於下端對象,要是依賴的話就要通過抽象來依賴。這是什么意思呢?意思就是上端對象如BLL層中,需要調用下端對象的DAL層時不能直接調用DAl的具體實現,而是通過抽象的方式來進行調用。這樣做是有一定的道理的。有這么一個場景,你們的項目本來是用Sqlserver來進行數據訪問的,那么就會有一個SqlserverDal對象。BLL層調用的時候通過new SqlserverDal(),直接創建一個SqlserverDal對象進行數據訪問,現在項目又要改為Mysql數據庫,用MysqlDal進行數據訪問。這時候就麻煩了,你的BLL層將new SqlserverDal()全部改為new MysqlDal()。同理BLL層也是這個道理。這么做,從程序的架構而言是相當不合理的,我只是想將SqlserverDal替換為MysqlDal。按道理說我只要添加MysqlDal對象就可以了。可現在的做法是還要將BLL中的new SqlserverDal()全部改一遍。這未免有點得不償失了。這時IOC就排上用場了,IOC的核心理念就是上端對象通過抽象來依賴下端對象,那么我們在BLL中,不能直接通過new SqlserverDal()來創建一個對象,而是通過結構來聲明(抽象的形式來進行依賴),當我們替換MysqlDal時我們只需讓MysqlDal也繼承這個接口,那么我們BLL層的邏輯就不用動了。那么現在又有一個問題,對象我們可以用接口來接收,所有子類出現的地方都可以用父類來替代,這沒毛病。但對象的創建還是要知道具體的類型,還是通過之前的new SqlserverDal()這種方式創建對象。肯定是不合理的,這里我們還是依賴於細節。
那我們需要怎么處理呢?這時候IOC容器就該上場了,IOC容器可以理解為一個第三方的類,專門為我們創建對象用的,它不需要關注具體的業務邏輯,也不關注具體的細節。你只需將你需要的創建的對象類型傳給它,它就能幫我們完成對象的創建。常見的IOC容器有Autofac,Unity
接觸.net core的小伙伴可能對容器很熟悉,.net core中將IOC容器內置了。創建對象需要先進行注冊
public void ConfigureServices(IServiceCollection services) { services.AddTransient<IHomeBll,HomeBll>(); services.AddTransient<Iservice,LoginService>(); }
從上面的示例我們可以看到.net core中是通過ServiceCollection容器幫我們完成對象的創建,我們只需將接口的類型和要創建對象的類型傳進去,它就能幫我們完成對象的創建。那么它的原理是啥呢,我們能不能創建自已的容器來幫我們完成對象的創建呢,讓我們帶着疑惑繼續往下走
一.容器雛形
這里我們先不考慮那么多,我們先寫一個容器,幫我們完成對象的創建工作。
public class HTContainer : IHTContainer { //創建一個Dictionary數據類型的對象用來存儲注冊的對象 private Dictionary<string, Type> TypeDictionary = new Dictionary<string, Type>(); //注冊方法,用接口的FullName為key值,value為要創建對象的類型 public void RegisterType<IT, T>() { this.TypeDictionary.Add(typeof(IT).FullName, typeof(T)); } //創建對象通過傳遞的類型進行匹配 public IT Resolve<IT>() { string key = typeof(IT).FullName; Type type = this.TypeDictionary[key]; //獲取要創建對象的類型 //這里先不考慮有參構造函數的問題,后面會逐一的解決這些問題 return (IT)Activator.CreateInstance(type); //通過反射完成對象的創建,這里我們先不考慮參數問題 } }
簡單調用
//實例化容器對象 IHTContainer container = new HTContainer(); //注冊對象 container.RegisterType<IDatabase,SqlserverDal>(); //通過容器完成對象的創建,不體現細節,用抽象完成對象的創建 IDatabase dal = container.Resolve<IDatabase>(); dal.Connection("con");
通過上邊的一頓操作,我們做了什么事呢?我們完成了一個大的飛躍,通常創建對象我們是直接new一個,現在我們是通過一個第三方的容器為我們創建對象,並且我們不用依賴於細節,通過接口的類型完成對象的創建,當我們要將SqlserverDal替換為MysqlDal時,我們只需要在注冊的時候將SqlserverDal替換為MysqlDal即可
二.升級改造容器(解決參數問題)
上面我們將傳統對象創建的方式,改為使用第三方容器來幫我們完成對象的創建。但這個容器考慮的還不是那么的全面,例如有參構造的問題,以及對象的依賴問題我們還沒有考慮到,接下來我們繼續完善這個容器,這里我們先不考慮多個構造函數的問題。這里先解決只有一個構造函數場景的參數問題
1.構造函數只有一個參數的情況
//創建對象通過傳遞的類型進行匹配 public IT Resolve<IT>() { string key = typeof(IT).FullName; Type type = this.TypeDictionary[key]; //獲取要創建對象的類型 var ctor = type.GetConstructors()[0]; //這里先考慮只有一個構造函數的場景 //一個參數的形式 var paraList = ctor.GetParameters(); var para = paraList[0]; Type paraInterfaceType = para.ParameterType; Type paraType = this.TypeDictionary[paraInterfaceType.FullName]; //還是要先獲取依賴對象的類型 object oPara = Activator.CreateInstance(paraType); //創建參數中所依賴的對象 return (IT)Activator.CreateInstance(type,oPara); //創建對象並傳遞所依賴的對象 }
2.構造函數多參數的情況
上面我們解決了構造函數只有一個參數的問題,我們是通過構造函數的類型創建一個對象,並將這個對象作為參數傳遞到要實例化的對象中。那么多參數我們就需要創建多個參數的對象傳遞到要實例的對象中
//創建對象通過傳遞的類型進行匹配 public IT Resolve<IT>() { string key = typeof(IT).FullName; Type type = this.TypeDictionary[key]; //獲取要創建對象的類型 var ctor = type.GetConstructors()[0]; //這里先考慮只有一個構造函數的場景 //多個參數的形式 List<object> paraList = new List<object>(); //聲明一個list來存儲參數類型的對象 foreach (var para in ctor.GetParameters()) { Type paraInterfaceType = para.ParameterType; Type paraType = this.TypeDictionary[paraInterfaceType.FullName]; object oPara = Activator.CreateInstance(paraType); paraList.Add(oPara); } return (IT)Activator.CreateInstance(type, paraList.ToArray()); //創建對象並傳遞所依賴的對象數組 }
3.解決對象的循環依賴問題
通過上面的兩步操作,我們已經能對構造函數中的參數初始化對象並傳遞到要實例的對象中,但這只是一個層級的。我們剛才做的只是解決了這么一個問題,假設我們要創建A對象,A對象依賴於B對象。我們做的就是創建了B對象作為參數傳遞給A並創建A對象,這只是一個層級的。當B對象又依賴於C對象,C對象又依賴於D對象,這么一直循環下去。這樣的場景我們該怎么解決呢?下面我們將通過遞歸的方式來解決這一問題
//創建對象通過傳遞的類型進行匹配 public IT Resolve<IT>() { return (IT)this.ResolveObject(typeof(IT)); } //通過遞歸的方式創建多層級的對象 private object ResolveObject(Type abstractType) { string key = abstractType.FullName; Type type = this.TypeDictionary[key]; //獲取要創建對象的類型 var ctor = type.GetConstructors()[0]; //多個參數的形式 List<object> paraList = new List<object>(); foreach (var para in ctor.GetParameters()) { Type paraInterfaceType = para.ParameterType; Type paraType = this.TypeDictionary[paraInterfaceType.FullName]; object oPara = ResolveObject(paraInterfaceType); //自已調用自己,實現遞歸操作,完成各個層級對象的創建 paraList.Add(oPara); } return (object)Activator.CreateInstance(type, paraList.ToArray()); }
三.繼續升級(考慮多個構造函數的問題)
上面我們只是考慮了只有一個構造函數的問題,那初始化的對象有多個構造函數我們該如何處理呢,我們可以像Autofac那樣選擇一個參數最多的構造函數,也可以像ServiceCollection那樣選擇一個參數的超集來進行構造,當然我們也可以聲明一個特性,那個構造函數中標記了這個特性,我們就采用那個構造函數。
//通過遞歸的方式創建多層級的對象 private object ResolveObject(Type abstractType) { string key = abstractType.FullName; Type type = this.TypeDictionary[key]; //獲取要創建對象的類型 var ctorArray = type.GetConstructors(); //獲取對象的所有構造函數 ConstructorInfo ctor = null; //判斷構造函數中是否標記了HTAttribute這個特性 if (ctorArray.Count(c => c.IsDefined(typeof(HTAttribute), true)) > 0) { //若標記了HTAttribute特性,默認就采用這個構造函數 ctor = ctorArray.FirstOrDefault(c => c.IsDefined(typeof(HTAttribute), true)); } else { //若都沒有標記特性,那就采用構造函數中參數最多的構造函數 ctor = ctorArray.OrderByDescending(c => c.GetParameters().Length).FirstOrDefault(); } //多個參數的形式 List<object> paraList = new List<object>(); foreach (var para in ctor.GetParameters()) { Type paraInterfaceType = para.ParameterType; Type paraType = this.TypeDictionary[paraInterfaceType.FullName]; object oPara = ResolveObject(paraInterfaceType); //自已調用自己,實現遞歸操作,完成各個層級對象的創建 paraList.Add(oPara); } return (object)Activator.CreateInstance(type, paraList.ToArray()); }
上面的操作我們通過依賴注入的方式完成了對容器的升級,那么依賴注入到底是啥呢?
依賴注入(Dependency Injection,簡稱DI)就是構造A對象時,需要依賴B對象,那么就先構造B對象作為參數傳遞到A對象,這種對象初始化並注入的技術就叫做依賴注入。IOC是一種設計模式,程序架構的目標。DI是IOC的實現手段