一、IOC
1.什么是IOC?
控制反轉(英語:Inversion of Control,縮寫為IoC),是[面向對象編程]中的一種設計原則,可以用來減低計算機代碼之間的[耦合度]其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI),還有一種方式叫“依賴查找”(Dependency Lookup).
IoC:是一種設計模式
DI:是踐行控制反轉思想的一種方式
2.為什么要用IOC
因為IoC 控制反轉是依賴抽象,而抽象是穩定的,不依賴細節,因為細節還可能會依賴其他細節,為了屏蔽細節,需要使用依賴注入去解決無限層級的對象依賴。
3.Net中常用的IoC容器
目前用的最多的是AutoFac和Castle,在.Net Core中框架內置了IOC容器,Unity和ObjectBuilder是相對比較久遠的框架,用的比較少。
-
AutoFac
-
Castle
-
Unity
-
ObjectBuilder
二、如何手寫實現?
1.基本設計
核心思想:
工廠 + 反射
首先我們想自己實現一個IoC容器其實並不難,我們在使用現有的IoC容器都知道,在使用前,我們需要先注冊,然后才能使用
所以我們將工廠換成手動注冊的方式,因為寫一大堆if else 或者switch也不太美觀,根據主流IoC的使用方式來以葫蘆畫瓢,如果后期繼續完善功能加入程序集注入的話,還是得實現一個工廠,來省略手動注冊
但是這次目標是實現一個簡易版的IoC容器,我們先實現基礎功能,待后面一步一步去完善,再加入一些新的功能,即我們不考慮性能或者擴展度,目的是循序漸進,在寫之前我們先整理出 實現步驟和實現方式
-
方便接入和擴展,我們在這先定義一個容器接口 IManualContainer
-
定義ManualContainer繼承實現IManualContainer
-
聲明一個靜態字典對象存儲注冊的對象
-
利用反射構建對象,考慮到性能可以加入Expression或者Emit的方式來做一些優化
public interface IManualContainer
{
void Register<TFrom, To>(string servicesName = null) where To : TFrom;
Tinterface Resolve<Tinterface>(string servicesName = null);
}
2.要實現的功能
1.基本對象構造
2.構造函數注入
3.多級依賴和多構造函數及自定義注入
4.屬性注入&方法注入
5.單接口多實現
三、編碼實現及思路剖析
1.實現構造對象(單接口注入)
1.首先實現接口來進行編碼私有字段 container用來存儲注冊的類型,key是對應接口的完整名稱,Value是需要Resolve的類型。
2.泛型約束保證需要被Resolve類型 (To) 實現或者繼承自注冊類型 (TFrom)
public class ManualContainer : IManualContainer
{
//存儲注冊類型
private static Dictionary<string, Type> container =
new Dictionary<string, Type>();
//注冊
public void Register<TFrom, To>(string servicesName = null) where To : TFrom
{
string Key = $"{typeof(TFrom).FullName}{servicesName}";
if (!container.ContainsKey(Key))
{
container.Add(Key, typeof(To));
}
}
1.實現構造對象,首先需要傳入被構造的類型的抽象接口T
2.在Resolve中根據T作為Key,在存儲容器中找到注冊時映射的類型,並通過反射構造對象
//構建對象
public TFrom Resolve<TFrom>(string servicesName = null)
{
string Key = $"{typeof(TFrom).FullName}{servicesName}";
container.TryGetValue(key, out Type target);
if(target is null)
{
return default(TFrom);
}
object t = Activator.CreateInstance(target);
}
}
1.首先我們准備需要的接口(ITestA)和實例(TestA)來利用容器來構造對象
public interface ITestA
{
void Run();
}
public class TestA : ITestA
{
public void Run()=> Console.WriteLine("這是接口ITestA的實現");
}
2.調用IoC容器來創建對象
IManualContainer container = new ManualContainer();
//注冊到容器中
container.Register<ITestA, TestA>();
ITestA instance = container.Resolve<ITestA>();
instance.Run();
//out put "這是接口ITestA的實現"
2.構造函數注入
1.假設我們的TestA類中需要ITestB接口的實例或者其他更多類型的實例,並且需要通過構造函數注入,我們應該如何去完善我們的IoC容器呢?
public class TestA : ITestA
{
private ITestB testB = null;
//構造函數
public TestA(ITestB testB)=> this.testB = testB;
public void Run()
{
this.testB.Run();
Console.WriteLine("這是接口ITestA的實現");
}
}
2.我們按照上面的步驟照常注冊和構造對象,發現報錯了,在Resolve
-
先定義List<object>集合存儲對象構造時需要的參數列表
-
通過需要被實例的目標類型找到類中的構造函數,暫不考慮多構造函數case
-
找到構造函數參數及類型,然后創建參數的實例加入List中,在反射構造時傳入參數就解決了
//完善Resolve構建對象函數
public TFrom Resolve<TFrom>(string servicesName = null)
{
string Key = $"{typeof(TFrom).FullName}{servicesName}";
container.TryGetValue(key, out Type target);
if(target is null)
{
return default(TFrom);
}
//存儲參數列表
List<object> paramList = new List<object>();
//找到目標類型的構造函數,暫不考慮多構造函數case
var ctor = target.GetConstructors().FirstOrDefault();
//找到參數列表
var ctorParams = ctor.GetParameters();
foreach (var item in ctorParams)
{
//參數類型
Type paramType = item.ParameterType;
string paramKey = paramType.FullName;
//找到參數注冊時映射的實例
container.TryGetValue(paramKey, out Type ParamType);
//構造出實例然后加入參數列表
paramList.Add(Activator.CreateInstance(ParamType));
}
object t = Activator.CreateInstance(target,paramList);
}
}
3.多級依賴(遞歸)
根據上面我們目前實現的結果來看,這是解決了構造函數和多參數注入以及基本的構造對象問題,那現在問題又來了
-
如果是很多層的依賴該怎么辦?
-
例如多個構造函數怎么辦呢?
-
在多個構造函數中用戶想自定義需要被注入的構造函數怎么辦?
總結3點問題
- 1.多級依賴問題
例如ITestB 的實例中依賴ITestC,一直無限依賴我們怎么解決呢?毫無疑問,做同樣的事情,但是要無限做下去,就使用 遞歸
,下面我們來改造我們的方法
-
2.多個構造函數
-
取參數最多的方式注入 (AutoFac)
-
取並集方式注入 (ASP .NET Core)
-
-
3.自定義注入
我們可以使用特性標記的方式來實現,用戶在需要被選擇注入的構造函數上加入特性標簽來完成
1.創建私有遞歸方法,這個方法的作用就是創建對象用
private object CreateInstance(Type type,string serviceName = null)
{
}
2.我們選擇第一種方式實現,修改之前獲取第一個構造函數的代碼,選擇最多參數注入
//找到目標類型的構造函數,找參數最多的構造函數
ConstructorInfo ctor = null;
var ctors = target.GetConstructors();
ctor = ctors.OrderByDescending(x => x.GetParameters().Length).First();
3.自定義特性CtorInjection,可以使用戶自定義選擇
//自定義構造函數注入標記
[AttributeUsage(AttributeTargets.Constructor)]
public class CtorInjectionAttribute : Attribute
{
}
4.最終代碼
private object CreateInstance(Type type,string serviceName = null)
{
string key = $"{ type.FullName }{serviceName}";
container.TryGetValue(key, out Type target);
//存儲參數列表
List<object> paramList = new List<object>();
ConstructorInfo ctor = null;
//找到被特性標記的構造函數作為注入目標
ctor = target.GetConstructors().FirstOrDefault(x => x.IsDefined(typeof(CtorInjectionAttribute), true));
//如果沒有被特性標記,那就取構造函數參數最多的作為注入目標
if (ctor is null)
{
ctor = target.GetConstructors().OrderByDescending(x => x.GetParameters().Length).First();
}
//找到參數列表
var ctorParams = ctor.GetParameters();
foreach (var item in ctorParams)
{
//參數類型
Type paramType = item.ParameterType;
//遞歸調用構建對象
object paramInstance = CreateInstance(paramType);
//構造出實例然后加入參數列表
paramList.Add(paramInstance);
}
object t = Activator.CreateInstance(target);
return t;
}
public TFrom Resolve<TFrom>()
{
return (TFrom)this.CreateInstance(typeof(TFrom));
}
4.屬性注入&方法注入
1.自定義特性PropInjection,可以使用戶自定義選擇
//自定義構造屬性注入標記
[AttributeUsage(AttributeTargets.Property)]
public class PropInjectionAttribute : Attribute
{
}
//自定義構造方法注入標記
[AttributeUsage(AttributeTargets.Method)]
public class MethodInjectionAttribute : Attribute
{
}
2.遍歷實例中被標記特性的屬性。
3.獲取屬性的類型,調用遞歸構造對象函數。
4.設置目標對象的屬性值。
5.方法注入也是同理,換湯不換葯而已
private object CreateInstance(Type type,string serviceName = null)
{
string key = $"{ type.FullName }{serviceName}";
container.TryGetValue(key, out Type target);
//存儲參數列表
List<object> paramList = new List<object>();
ConstructorInfo ctor = null;
//找到被特性標記的構造函數作為注入目標
ctor = target.GetConstructors().FirstOrDefault(x => x.IsDefined(typeof(CtorInjectionAttribute), true));
//如果沒有被特性標記,那就取構造函數參數最多的作為注入目標
if (ctor is null)
{
ctor = target.GetConstructors().OrderByDescending(x => x.GetParameters().Length).First();
}
//找到參數列表
var ctorParams = ctor.GetParameters();
foreach (var item in ctorParams)
{
//參數類型
Type paramType = item.ParameterType;
//遞歸調用構建對象
object paramInstance = CreateInstance(paramType);
//構造出實例然后加入參數列表
paramList.Add(paramInstance);
}
object t = Activator.CreateInstance(target);
//獲取目標類型的被特性標記的屬性<屬性注入>
var propetys = target.GetProperties().Where(x => x.IsDefined(typeof(PropInjectionAttribute), true));
foreach (var item in propetys)
{
//獲取屬性類型
Type propType = item.PropertyType;
object obj = this.CreateInstance(propType);
//設置值
item.SetValue(t, obj);
}
//獲取目標類型的被特性標記的方法<方法注入>
var methods = target.GetMethods().Where(x => x.IsDefined(typeof(MethodInjectionAttribute), true)).ToList();
foreach (var item in methods)
{
List<object> methodParams = new List<object>();
foreach (var p in item.GetParameters())
{
//獲取方法參數類型
Type propType = p.ParameterType;
object obj = this.CreateInstance(propType);
methodParams.Add(obj);
}
item.Invoke(t, methodParams);
}
return t;
}
public TFrom Resolve<TFrom>()
{
return (TFrom)this.CreateInstance(typeof(TFrom));
}
5.單接口多實現
假設我們一個接口被多個實例實現,我們需要注入怎么操作呢?
毫無疑問我們應該想到在注入時取一個別名
,沒錯正是這種方法,但是也會存在一個問題,我們取了別名之后只能解決注入的場景?那依賴接口的地方如何知道注入哪一個實例呢?
1.注冊單接口多實例
container.Register<ITest, test1>("test1");
container.Register<ITest, test2>("test2");
ITest instance = container.Resolve<ITest>("test1");
ITest instance1 = container.Resolve<ITest>("test2");
2.創建標記特性,用來標記目標對象
[AttributeUsage(AttributeTargets.Parameter| AttributeTargets.Property)]
public class ParamterInjectAttribute : Attribute
{
public ParamterInjectAttribute(string nickName) => NickName = nickName;
public string NickName { get; private set; }
}
3.我們需要在依賴“單接口多實例的類中”使用時告訴參數,我們需要的實例,依然使用參數特性標記
public class use : IUse
{
private ITest _Test = null;
//告訴構造函數依賴ITest接口時使用別名為test1的實例
public BLL1([ParamterInjectAttribute("test1")] ITest接口時使用別名為test1的實例 Test)
{
this._Test = Test;
}
}
4.在構造對象時查找是否被特性標記,然后構造對象
foreach (var item in ctor.GetParameters())
{
Type paramType = item.ParameterType;
string nickName = string.Empty;
//查找構造函數的參數是否被特性標記
if (item.IsDefined(typeof(ParamterInjectAttribute), true))
{
//找到被標記需要構造的實例別名
nickName = item.GetCustomAttribute<ParamterInjectAttribute>().NickName;
}
//根據別名創建對象
object instance = CreateInstance(paramType,nickName);
if (instance != null)
{
paramList.Add(instance);
}
}
四、總結
目前還不是很完善,只是實現了屬性,方法,以及構造函數注入,很多必要功能還沒有,下一步將在現有代碼基礎上利用Emit的方式來創建對象,加入基本的驗證環節以提高健壯性,加入生命周期管理,和AOP擴展。
第一版最終代碼
public class ManualContainer : IManualContainer
{
private static Dictionary<string, Type> container = new Dictionary<string, Type>();
public void Register<TFrom, To>(string servicesName = null) where To : TFrom
{
string Key = $"{typeof(TFrom).FullName}{servicesName}";
if (!container.ContainsKey(Key))
{
container.Add(Key, typeof(To));
}
}
public Tinterface Resolve<Tinterface>(string serviceName = null)
{
return (Tinterface)this.CreateInstance(typeof(Tinterface), serviceName);
}
private object CreateInstance(Type type, string serviceName = null)
{
string key = $"{ type.FullName }{serviceName}";
container.TryGetValue(key, out Type target);
object instance = ctorInjection(target);
propInjection(target, instance);
methodInjection(target, instance);
return instance;
}
/// <summary>
/// 構造函數注入
/// </summary>
/// <param name="target">需要被創建的類型</param>
/// <returns>被創建的實例</returns>
private object ctorInjection(Type targetType)
{
List<object> paramList = new List<object>();
ConstructorInfo ctor = null;
ctor = targetType.GetConstructors().FirstOrDefault(x => x.IsDefined(typeof(CtorInjectionAttribute), true));
if (ctor is null)
{
ctor = targetType.GetConstructors().OrderByDescending(x => x.GetParameters().Length).First();
}
foreach (var item in ctor.GetParameters())
{
Type paramType = item.ParameterType;
string nickName = GetNickNameByAttribute(item);
object instance = CreateInstance(paramType, nickName);
if (instance != null)
{
paramList.Add(instance);
}
}
object t = Activator.CreateInstance(targetType, paramList.ToArray());
return t;
}
/// <summary>
/// 屬性注入
/// </summary>
/// <param name="targetType">需要被創建的類型</param>
/// <param name="sourceType">根據需要被創建的類型構造出的實例</param>
private void propInjection(Type targetType, object inststance)
{
var propetys = targetType.GetProperties().Where(x => x.IsDefined(typeof(PropInjectionAttribute), true));
foreach (var item in propetys)
{
Type propType = item.PropertyType;
string nickName = GetNickNameByAttribute(item);
object obj = this.CreateInstance(propType, nickName);
item.SetValue(inststance, obj);
}
}
/// <summary>
/// 方法注入
/// </summary>
/// <param name="targetType">需要被創建的類型</param>
/// <param name="sourceType">根據需要被創建的類型構造出的實例</param>
private void methodInjection(Type targetType, object inststance)
{
List<object> methodParams = new List<object>();
var methods = targetType.GetMethods().Where(x => x.IsDefined(typeof(MethodInjectionAttribute), true)).ToList();
foreach (var item in methods)
{
foreach (var p in item.GetParameters())
{
Type propType = p.ParameterType;
string nickName = GetNickNameByAttribute(item);
object obj = this.CreateInstance(propType, nickName);
methodParams.Add(obj);
}
item.Invoke(inststance, methodParams.ToArray());
}
}
private string GetNickNameByAttribute(ICustomAttributeProvider provider)
{
if (provider.IsDefined(typeof(ParamterInjectAttribute), true))
{
ParamterInjectAttribute attribute = provider.GetCustomAttributes(typeof(ParamterInjectAttribute), true)[0] as ParamterInjectAttribute;
return attribute.NickName;
}
return string.Empty;
}
}