手寫IOC容器

world發表於2020-07-26

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的實現手段

 

相關文章