如何編寫一個簡單的依賴注入容器

隨著大規模的項目越來越多,許多項目都引入了依賴注入框架,其中最流行的有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的處理

在確定了索引類型后,RegisterResolve的處理都應該隨之改變。

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_alogger_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(); }

這段代碼通過反射獲取了構造函數中的所有參數,並對每個參數使用ResolveResolveMany解決。

值得注意的是參數的解決是延遲的,只有在構建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等,而是直接使用微軟提供的標準介面。

雖然具體的實現方式離我們原來越遠,但是了解一下它們的原理總是有好處的。

Advertisements

你可能會喜歡