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


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

在確定了索引類型后,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等,而是直接使用微軟提供的標准接口。
雖然具體的實現方式離我們原來越遠,但是了解一下它們的原理總是有好處的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM