隨著大規模的項目越來越多,許多項目都引入了依賴註入框架,其中最流行的有Castle Windsor, Autofac和Unity Container。 微軟在最新版的Asp.Net Core中自帶了依賴註入的功能,有興趣可以 "查看這裡" 。 關於什麼是依賴註入容器網上已經有很多的文章介紹,這裡我將 ...
隨著大規模的項目越來越多,許多項目都引入了依賴註入框架,其中最流行的有Castle Windsor, Autofac和Unity Container。
微軟在最新版的Asp.Net Core中自帶了依賴註入的功能,有興趣可以查看這裡。
關於什麼是依賴註入容器網上已經有很多的文章介紹,這裡我將重點講述如何實現一個自己的容器,可以幫助你理解依賴註入的原理。
容器的構想
在編寫容器之前,應該先想好這個容器如何使用。
容器允許註冊服務和實現類型,允許從服務類型得出服務的實例,它的使用代碼應該像
var container = new Container();
container.Register<MyLogger, ILogger>();
var logger = container.Resolve<ILogger>();
最基礎的容器
在上面的構想中,Container類有兩個函數,一個是Register
,一個是Resolve
。
容器需要在Register
時關聯ILogger
介面到MyLogger
實現,並且需要在Resolve
時知道應該為ILogger
生成MyLogger
的實例。
以下是實現這兩個函數最基礎的代碼
public class Container
{
// service => implementation
private IDictionary<Type, Type> TypeMapping { get; set; }
public Container()
{
TypeMapping = new Dictionary<Type, Type>();
}
public void Register<TImplementation, TService>()
where TImplementation : TService
{
TypeMapping[typeof(TService)] = typeof(TImplementation);
}
public TService Resolve<TService>()
{
var implementationType = TypeMapping[typeof(TService)];
return (TService)Activator.CreateInstance(implementationType);
}
}
Container
在內部創建了一個服務類型(介面類型)到實現類型的索引,Resolve
時使用索引找到實現類型並創建實例。
這個實現很簡單,但是有很多問題,例如
- 一個服務類型不能對應多個實現類型
- 沒有對實例進行生命周期管理
- 沒有實現構造函數註入
改進容器的構想 - 類型索引類型
要讓一個服務類型對應多個實現類型,可以把TypeMapping
改為
IDictionary<Type, IList<Type>> TypeMapping { get; set; }
如果另外提供一個保存實例的變數,也能實現生命周期管理,但顯得稍微複雜了。
這裡可以轉換一下思路,把{服務類型=>實現類型}改為{服務類型=>工廠函數},讓生命周期的管理在工廠函數中實現。
IDictionary<Type, IList<Func<object>>> Factories { get; set; }
有時候我們會想讓用戶在配置文件中切換實現類型,這時如果把鍵類型改成服務類型+字元串,實現起來會簡單很多。
Resolve可以這樣用: Resolve<Service>(serviceKey: Configuration["ImplementationName"])
IDictionary<Tuple<Type, string>, IList<Func<object>>> Factories { get; set; }
改進容器的構想 - Register和Resolve的處理
在確定了索引類型後,Register
和Resolve
的處理都應該隨之改變。
Register
註冊時應該首先根據實現類型生成工廠函數,再把工廠函數加到服務類型對應的列表中。
Resolve
解決時應該根據服務類型找到工廠函數,然後執行工廠函數返回實例。
改進後的容器
這個容器新增了一個ResolveMany
函數,用於解決多個實例。
另外還用了Expression.Lambda
編譯工廠函數,生成效率會比Activator.CreateInstance
快數十倍。
public class Container
{
private IDictionary<Tuple<Type, string>, IList<Func<object>>> Factories { get; set; }
public Container()
{
Factories = new Dictionary<Tuple<Type, string>, IList<Func<object>>>();
}
public void Register<TImplementation, TService>(string serviceKey = null)
where TImplementation : TService
{
var key = Tuple.Create(typeof(TService), serviceKey);
IList<Func<object>> factories;
if (!Factories.TryGetValue(key, out factories))
{
factories = new List<Func<object>>();
Factories[key] = factories;
}
var factory = Expression.Lambda<Func<object>>(Expression.New(typeof(TImplementation))).Compile();
factories.Add(factory);
}
public TService Resolve<TService>(string serviceKey = null)
{
var key = Tuple.Create(typeof(TService), serviceKey);
var factory = Factories[key].Single();
return (TService)factory();
}
public IEnumerable<TService> ResolveMany<TService>(string serviceKey = null)
{
var key = Tuple.Create(typeof(TService), serviceKey);
IList<Func<object>> factories;
if (!Factories.TryGetValue(key, out factories))
{
yield break;
}
foreach (var factory in factories)
{
yield return (TService)factory();
}
}
}
改進後的容器仍然有以下的問題
- 沒有對實例進行生命周期管理
- 沒有實現構造函數註入
實現實例的單例
以下麵代碼為例
var logger_a = container.Resolve<ILogger>();
var logger_b = container.Resolve<ILogger>();
使用上面的容器執行這段代碼時,logger_a
和logger_b
是兩個不同的對象,如果想要每次Resolve
都返回同樣的對象呢?
我們可以對工廠函數進行包裝,藉助閉包(Closure)的力量可以非常簡單的實現。
private Func<object> WrapFactory(Func<object> originalFactory, bool singleton)
{
if (!singleton)
return originalFactory;
object value = null;
return () =>
{
if (value == null)
value = originalFactory();
return value;
};
}
添加這個函數後在Register
中調用factory = WrapFactory(factory, singleton);
即可。
完整代碼將在後面放出,接下來再看如何實現構造函數註入。
實現構造函數註入
以下麵代碼為例
public class MyLogWriter : ILogWriter
{
public void Write(string str)
{
Console.WriteLine(str);
}
}
public class MyLogger : ILogger
{
ILogWriter _writer;
public MyLogger(ILogWriter writer)
{
_writer = writer;
}
public void Log(string message)
{
_writer.Write("[ Log ] " + message);
}
}
static void Main(string[] args)
{
var container = new Container();
container.Register<MyLogWriter, ILogWriter>();
container.Register<MyLogger, ILogger>();
var logger = container.Resolve<ILogger>();
logger.Log("Example Message");
}
在這段代碼中,MyLogger
構造時需要一個ILogWriter
的實例,但是這個實例我們不能直接傳給它。
這樣就要求容器可以自動生成ILogWriter
的實例,再傳給MyLogger
以生成MyLogger
的實例。
要實現這個功能需要使用c#中的反射機制。
把上面代碼中的
var factory = Expression.Lambda<Func<object>>(Expression.New(typeof(TImplementation))).Compile();
換成
private Func<object> BuildFactory(Type type)
{
// 獲取類型的構造函數
var constructor = type.GetConstructors().FirstOrDefault();
// 生成構造函數中的每個參數的表達式
var argumentExpressions = new List<Expression>();
foreach (var parameter in constructor.GetParameters())
{
var parameterType = parameter.ParameterType;
if (parameterType.IsGenericType &&
parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
// 等於調用this.ResolveMany<TParameter>();
argumentExpressions.Add(Expression.Call(
Expression.Constant(this), "ResolveMany",
parameterType.GetGenericArguments(),
Expression.Constant(null, typeof(string))));
}
else
{
// 等於調用this.Resolve<TParameter>();
argumentExpressions.Add(Expression.Call(
Expression.Constant(this), "Resolve",
new [] { parameterType },
Expression.Constant(null, typeof(string))));
}
}
// 構建new表達式並編譯到委托
var newExpression = Expression.New(constructor, argumentExpressions);
return Expression.Lambda<Func<object>>(newExpression).Compile();
}
這段代碼通過反射獲取了構造函數中的所有參數,並對每個參數使用Resolve
或ResolveMany
解決。
值得註意的是參數的解決是延遲的,只有在構建MyLogger
的時候才會構建MyLogWriter
,這樣做的好處是註入的實例不一定需要是單例。
用表達式構建的工廠函數解決的時候的性能會很高。
完整代碼
容器和示例的完整代碼如下
public interface ILogWriter
{
void Write(string text);
}
public class MyLogWriter : ILogWriter
{
public void Write(string str)
{
Console.WriteLine(str);
}
}
public interface ILogger
{
void Log(string message);
}
public class MyLogger : ILogger
{
ILogWriter _writer;
public MyLogger(ILogWriter writer)
{
_writer = writer;
}
public void Log(string message)
{
_writer.Write("[ Log ] " + message);
}
}
static void Main(string[] args)
{
var container = new Container();
container.Register<MyLogWriter, ILogWriter>();
container.Register<MyLogger, ILogger>();
var logger = container.Resolve<ILogger>();
logger.Log("asdasdas");
}
public class Container
{
private IDictionary<Tuple<Type, string>, IList<Func<object>>> Factories { get; set; }
public Container()
{
Factories = new Dictionary<Tuple<Type, string>, IList<Func<object>>>();
}
private Func<object> WrapFactory(Func<object> originalFactory, bool singleton)
{
if (!singleton)
return originalFactory;
object value = null;
return () =>
{
if (value == null)
value = originalFactory();
return value;
};
}
private Func<object> BuildFactory(Type type)
{
// 獲取類型的構造函數
var constructor = type.GetConstructors().FirstOrDefault();
// 生成構造函數中的每個參數的表達式
var argumentExpressions = new List<Expression>();
foreach (var parameter in constructor.GetParameters())
{
var parameterType = parameter.ParameterType;
if (parameterType.IsGenericType &&
parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
// 等於調用this.ResolveMany<TParameter>();
argumentExpressions.Add(Expression.Call(
Expression.Constant(this), "ResolveMany",
parameterType.GetGenericArguments(),
Expression.Constant(null, typeof(string))));
}
else
{
// 等於調用this.Resolve<TParameter>();
argumentExpressions.Add(Expression.Call(
Expression.Constant(this), "Resolve",
new [] { parameterType },
Expression.Constant(null, typeof(string))));
}
}
// 構建new表達式並編譯到委托
var newExpression = Expression.New(constructor, argumentExpressions);
return Expression.Lambda<Func<object>>(newExpression).Compile();
}
public void Register<TImplementation, TService>(string serviceKey = null, bool singleton = false)
where TImplementation : TService
{
var key = Tuple.Create(typeof(TService), serviceKey);
IList<Func<object>> factories;
if (!Factories.TryGetValue(key, out factories))
{
factories = new List<Func<object>>();
Factories[key] = factories;
}
var factory = BuildFactory(typeof(TImplementation));
WrapFactory(factory, singleton);
factories.Add(factory);
}
public TService Resolve<TService>(string serviceKey = null)
{
var key = Tuple.Create(typeof(TService), serviceKey);
var factory = Factories[key].Single();
return (TService)factory();
}
public IEnumerable<TService> ResolveMany<TService>(string serviceKey = null)
{
var key = Tuple.Create(typeof(TService), serviceKey);
IList<Func<object>> factories;
if (!Factories.TryGetValue(key, out factories))
{
yield break;
}
foreach (var factory in factories)
{
yield return (TService)factory();
}
}
}
寫在最後
這個容器實現了一個依賴註入容器應該有的主要功能,但是還是有很多不足的地方,例如
- 不支持線程安全
- 不支持非泛型的註冊和解決
- 不支持只用於指定範圍內的單例
- 不支持成員註入
- 不支持動態代理實現AOP
我在ZKWeb網頁框架中也使用了自己編寫的容器,只有300多行但是可以滿足實際項目的使用。
完整的源代碼可以查看這裡和這裡。
微軟從.Net Core開始提供了DependencyInjection的抽象介面,這為依賴註入提供了一個標準。
在將來可能不會再需要學習Castle Windsor, Autofac等,而是直接使用微軟提供的標準介面。
雖然具體的實現方式離我們原來越遠,但是瞭解一下它們的原理總是有好處的。