白話系列之IOC,三個類實現簡單的Ioc

  • 2019 年 10 月 3 日
  • 筆記

前言:部落格園上已經有很多IOC的部落格.而且很多寫的很好,達到開源的水平,但是對於很多新人來說,只了解ioc的概念,以及怎麼去使用ioc.然後想更進一步去看源碼,但是大部分源碼都比較困難,當不知道一個框架整體時候,從每一個片段去推理,其實很耗費時間,所以這篇部落格,從autofac及.netcore自帶的ioc的源碼中抽象出最核心程式碼,先了解整個ioc的實現方式,其他的所有好的ioc,只是在這個框架上面進行縫縫補補.

友情提示下,這個ioc雖然能夠使用,但是只是為了做例子,所以只保留最核心程式碼,要使用還是使用autofac或成熟的ioc框架.

一:老生常談

問:什麼是ioc?

答:依賴注入,是一種思想,由於過分模糊,所以提出DI的觀點:被注入對象依賴IOC容器配置依賴對象

問:有什麼用?

答:解決高耦合問題,以前沒有ioc的時候,每次都執行的是new操作,這沒什麼不好,但是假設,本來使用sqlserver,通過IConnection  conn = new Sqlserver();方式初始化所有的連接操作,但是現在老闆要求改成mysql當做資料庫,如果按照new的方式,得一個個去改,全局搜索,全局替換,其實也是可以的,無非是人累點,還需要一遍遍去檢查,看哪裡漏了,這時候就懷念Ioc的好處了,只需在容器內改變一處,便全局改變.當然,這裡並不是少寫了幾行new程式碼,程式碼還是一樣的多,只不過new的操作讓容器去處理了.擬人化的方式就是,new的方式就相當於以前沒群的時候,你本來是密令是10, 你一個個去通知你所想要通知的人即new,但是現在呢,密令被敵人偷聽去了,你需要更改,這次改成20,你就得一個個通知,但是現在你每次聯繫別人都是通過手機去聯繫,你不需要管手機是怎麼發送給對方的,只需要知道你給手機一個通知,其他人都可以立馬收到,那麼手機在這裡扮演的就是容器的概念,一次更改,全部獲悉

二:理論結束,開始思考準備ioc之前需要準備的東西

1.首先建立一個收集器,收集可能需要new的對象,那麼會有幾種生命周期去new一個對象?

常用的就是單例模式(singleton), 每次直接new對象,即用即拋(Transient),還有當前請求的主執行緒中只會創建一個對象(Scope,注意,單例是所有請求都會公用一個對象),所以,先定義介面,如下,命名即功能

    public interface IServiceCollection      {          IServiceCollection AddTransient<T1, T2>() where T2 : T1;          IServiceCollection AddTransient<T1>(T1 t2);          IServiceCollection AddSingleton<T1, T2>() where T2 : T1;          IServiceCollection AddSingleton<T1>(T1 t2);          IServiceCollection AddScoped<T1, T2>() where T2 : T1;          IServiceCollection AddScoped<T1>(T1 t2);          IServiceProvider BuildServiceProvider();      }

 2.其次,建立一個對象提供器,獲取容器內的可以獲取的對象

    越簡單越好,直接通過類型獲取對應的對象,同樣,介面定義如下:

    public interface IServiceProvider      {          T GetRequiredService<T>();          Object GetRequiredService(Type type);      }

3.Collection對收集的對象進行保存,並且需要對每個對象進行區分是Singleton,scoped,還是transient的

注意:我覺得在設計一個好的程式碼時候,得弄清楚當前類型具體的作用,然後如果作用不一樣,那麼得重新創建一個類型,當然如果後期發現沒必要,可以合併,但是前期還是得分清楚點,就如sql中的範式及反範式.

 3.1:首先定義枚舉,區分當前的類型需要new的類型,與上文中的一致

 

    public enum ServiceLifetime      {          Singleton = 0,          Transient = 1,          Scoped = 2      }

 

  3.2:其次需要保存注入進去的類型及周期,因為不去考慮架構,只考慮那ioc的意思,就盡量簡化程式碼

 

三:直接開始擼程式碼

 

1.通過Type創建對象,先默認只創建當前無參構造器,程式碼很簡單

 

        public Object GetCache(IDictionary<Type, IServiceCache> typePairs)          {              if (_obj == null)              {                  _obj = Activator.CreateInstance(_type);              }              switch (_typeEnum)              {                  case ServiceLifetime.Transient:                      return Activator.CreateInstance(_type);                  case ServiceLifetime.Singleton:                      return _obj;                  case ServiceLifetime.Scoped:                      throw new Exception("目前不支援scoped");                  default:                      throw new Exception("請傳遞正確生命周期");              }          }

 

 

 

DeepClone的寫法就是通過序列化的方式實現的,JsonConvert

      public static Object DeepClone(this Object obj, Type type)          {              return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj), type);          }

2.collection保存對應的對象,繼承IServiceCollection介面

 

    public class ServiceCollection : IServiceCollection      {          private ConcurrentDictionary<Type, IServiceCache> _typePairs;          public ServiceCollection()          {              _typePairs = new ConcurrentDictionary<Type, IServiceCache>();          }          public IServiceCollection AddScoped<T1, T2>() where T2 : T1{}          public IServiceCollection AddScoped<T1>(T1 t2){}          public IServiceCollection AddSingleton<T1, T2>() where T2 : T1{}          public IServiceCollection AddTransient<T1, T2>() where T2 : T1{}          public IServiceCollection AddSingleton<T>(T t2){}          public IServiceProvider BuildServiceProvider(){}      }

 實現Singleton及Transient,此處Scoped有些額外的語法糖,等後期會猜想實現

       public IServiceCollection AddSingleton<T1, T2>() where T2 : T1          {              Type t1 = typeof(T1);              Type t2 = typeof(T2);              ServiceTypeCache service = new ServiceTypeCache(t2, ServiceLifetime.Singleton);              if (!_typePairs.TryAdd(t1, service))              {                  throw new Exception("在注入對象時,有相同對象存在");              }              return this;          }          public IServiceCollection AddTransient<T1, T2>() where T2 : T1          {              Type t1 = typeof(T1);              Type t2 = typeof(T2);              ServiceTypeCache service = new ServiceTypeCache(t2, ServiceLifetime.Transient);              if (!_typePairs.TryAdd(t1, service))              {                  throw new Exception("在注入對象時,有相同對象存在");              }              return this;          }

 3:實現IServiceProvider介面,就是從Cache中獲取對應的對象

    public class ServiceProvider : IServiceProvider      {          private IDictionary<Type, IServiceCache> _cache;          public ServiceProvider(IDictionary<Type, IServiceCache> valuePairs)          {              _cache = valuePairs;          }          public T GetRequiredService<T>()          {              Type t = typeof(T);              return (T)GetRequiredService(t);          }          public object GetRequiredService(Type type)          {              IServiceCache service = null;              if (!_cache.TryGetValue(type, out service))              {                  throw new Exception("獲取參數對象沒有注入");              }              return service.GetCache();          }      }

 

 4:將Collection轉變為ServiceProvider

        public IServiceProvider BuildServiceProvider()          {              return new ServiceProvider(_typePairs);          }

5:OK,現在來試試這種簡單注入

    public interface ITestTransient      {          void Write();      }      public class TestATransient : ITestTransient      {          public void Write()          {              Console.WriteLine("----------------A----------------");          }      }      public class TestBTransient : ITestTransient      {          public void Write()          {              Console.WriteLine("----------------B----------------");          }      }

 

class Program      {          static void Main(string[] args)          {              InitA();              InitB();              Console.Read();          }          public static void InitA()          {              IServiceCollection collection = new ServiceCollection();              collection.AddTransient<ITestTransient, TestATransient>();              IServiceProvider provider =  collection.BuildServiceProvider();              provider.GetRequiredService<ITestTransient>().Write();          }          public static void InitB()          {              IServiceCollection collection = new ServiceCollection();              collection.AddTransient<ITestTransient, TestBTransient>();              IServiceProvider provider = collection.BuildServiceProvider();              provider.GetRequiredService<ITestTransient>().Write();          }      }

 

測試OK,只要在後面的程式碼中使用同一個provider,那麼從IOC容器中獲取的實例都是相同,改一處便全部都能修改

6.延伸,現在通過構造器注入其他程式碼,比如 

class A{}  class B  {     public B(A a) { }  }

猜想下,遇到這種構造器注入時候,怎麼去處理,其實和創建Type對象一直,通過CreateInstance(Type, Object[] param);去創建,param是每個需要注入的類型對象

OK,那我們來改下程式碼,將獲取Object對象的方法添加參數,因為構造器裡面注入的參數都是從IOC裡面獲取

    public interface IServiceCache      {          Object GetCache(IDictionary<Type, IServiceCache> typePairs);      }

獲取當前Type類型的構造器,默認獲取參數最多的,參數一樣多的,獲取最後一個,注:這裡可以添加一個特性,標明優先構造這個構造器,自己添加就好,寫法盡量簡單

        private List<Type> GetConstructor()          {              ConstructorInfo[] a = _type.GetConstructors();              ConstructorInfo b = null;              Int32 length = 0;              foreach (ConstructorInfo info in a)              {                  if (info.GetParameters().Length >= length)                  {                      b = info;                  }              }              ParameterInfo[] pa = b.GetParameters();              List<Type> list = new List<Type>();              foreach (var p in pa)              {                  list.Add(p.ParameterType);              }              return list;          }

構造器參數,就需要從typePairs裡面獲取,注意,這裡的所有參數都必須從IOC容器中獲取,當然這裡會有一個問題就是相互引用,這時候就需要注意下

        public Object GetCache(IDictionary<Type, IServiceCache> typePairs)          {              if (_obj == null)              {
          //這裡實際是構建一個表達式樹,這樣就不需要每次去通過反射創建對象了 List
<Type> types = GetConstructor(); Object[] paramters = types.ConvertAll(item => typePairs[item].GetCache(typePairs)).ToArray(); _obj = Activator.CreateInstance(_type, paramters); } switch (_typeEnum){...} }

7.測試

    public class ConstructorIOCTest      {          private readonly ITestTransient m_test;          public ConstructorIOCTest(ITestTransient test)          {              m_test = test;          }          public void WriteTestTransient()          {              m_test.Write();              Console.WriteLine("--------------ConstructorIOCTest-----------");          }      }

    class Program      {          static void Main(string[] args)          {              InitA();              InitB();              Console.Read();          }          public static void InitA()          {              IServiceCollection collection = new ServiceCollection();              collection.AddTransient<ITestTransient, TestATransient>();              collection.AddTransient<ConstructorIOCTest, ConstructorIOCTest>();              IServiceProvider provider =  collection.BuildServiceProvider();              provider.GetRequiredService<ConstructorIOCTest>().WriteTestTransient();          }          public static void InitB()          {              IServiceCollection collection = new ServiceCollection();              collection.AddTransient<ITestTransient, TestBTransient>();              collection.AddTransient<ConstructorIOCTest, ConstructorIOCTest>();              IServiceProvider provider = collection.BuildServiceProvider();              provider.GetRequiredService<ConstructorIOCTest>().WriteTestTransient();          }      }

 

 可以看出來,所有的IOC都是從構造器出發,這樣就避免到處修改的尷尬了

總結:

1.這是一個簡單的IOC程式碼,裡面我盡量採用最簡單的小白的方式去實現,沒有使用設計模式(本身最多有個工廠模式),沒有表達式樹,沒有鎖(鎖是非常重要的,後期我會花幾個章節去介紹各種鎖)

2.IOC其實就是一個概念,理解之後,在構造的時候添加幾個特性,比如屬性注入,方法注入,其實無非就是在ServiceTypeCache類中添加構造器,方法,屬性篩選之類的語法糖而已

3.這裡沒有時間Scopd的生命周期,因為我並不是很確定.net core中這個的寫法,對我來說有2種,一種是在GetService時候,HttpContext注入,一種是將ServiceProvider裡面進行包裝一層Guid,相同的Guid的Scopd相同

4.希望大家可以去看看源碼,尤其是推薦微軟開源的幾個框架,程式碼之精華,越看越覺得程式碼之美,雖然裡面很多程式碼就是在打修補程式,坑死人

5.https://github.com/BestHYC/IOCSolution.git,源碼,程式碼的話我就不加工了,因為沒什麼好加工的,畢竟IOC實在太成熟了