如何編寫一個簡單的依賴注入容器
隨著大規模的項目越來越多,許多項目都引入了依賴注入框架,其中最流行的有Castle Windsor, Autofac和Unity Container。
微軟在最新版的Asp.Net Core中自帶了依賴注入的功能,有興趣可以查看這裡。
關於什麼是依賴注入容器網上已經有很多的文章介紹,這裡我將重點講述如何實現一個自己的容器,可以幫助你理解依賴注入的原理。
容器的構想
在編寫容器之前,應該先想好這個容器如何使用。
容器允許註冊服務和實現類型,允許從服務類型得出服務的實例,它的使用代碼應該像
var container = new Container(); container.Register<MyLogger, ILogger>(); var logger = container.Resolve<ILogger>();
Advertisements
最基礎的容器
在上面的構想中,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); } }
Advertisements
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等,而是直接使用微軟提供的標準介面。
雖然具體的實現方式離我們原來越遠,但是了解一下它們的原理總是有好處的。