白話系列之IOC,三個類實現簡單的Ioc
前言:部落格園上已經有很多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實在太成熟了
&n