随着大规模的项目越来越多,许多项目都引入了依赖注入框架,其中最流行的有Castle Windsor, Autofac和Unity Container。
微软在最新版的Asp.Net Core中自带了依赖注入的功能,有兴趣可以查看这里。
关于什么是依赖注入容器网上已经有很多的文章介绍,这里我将重点讲述如何实现一个自己的容器,可以帮助你理解依赖注入的原理。
容器的构想
在编写容器之前,应该先想好这个容器如何使用。
容器允许注册服务和实现类型,允许从服务类型得出服务的实例,它的使用代码应该像
var container = new Container();
container.Register();
var logger = container.Resolve();
最基础的容器
在上面的构想中,Container类有两个函数,一个是Register
,一个是Resolve
。
容器需要在Register
时关联ILogger
接口到MyLogger
实现,并且需要在Resolve
时知道应该为ILogger
生成MyLogger
的实例。
以下是实现这两个函数最基础的代码
public class Container
{
// service => implementation
private IDictionary TypeMapping { get; set; }
public Container()
{
TypeMapping = new Dictionary();
}
public void Register()
where TImplementation : TService
{
TypeMapping[typeof(TService)] = typeof(TImplementation);
}
public TService Resolve()
{
var implementationType = TypeMapping[typeof(TService)];
return (TService)Activator.CreateInstance(implementationType);
}
}
Container
在内部创建了一个服务类型(接口类型)到实现类型的索引,Resolve
时使用索引找到实现类型并创建实例。
这个实现很简单,但是有很多问题,例如
- 一个服务类型不能对应多个实现类型
- 没有对实例进行生命周期管理
- 没有实现构造函数注入
改进容器的构想 - 类型索引类型
要让一个服务类型对应多个实现类型,可以把TypeMapping
改为
IDictionary> TypeMapping { get; set; }
如果另外提供一个保存实例的变量,也能实现生命周期管理,但显得稍微复杂了。
这里可以转换一下思路,把{服务类型=>实现类型}改为{服务类型=>工厂函数},让生命周期的管理在工厂函数中实现。
IDictionary>> Factories { get; set; }
有时候我们会想让用户在配置文件中切换实现类型,这时如果把键类型改成服务类型+字符串,实现起来会简单很多。
Resolve可以这样用: Resolve
IDictionary, IList>> Factories { get; set; }
改进容器的构想 - Register和Resolve的处理
在确定了索引类型后,Register
和Resolve
的处理都应该随之改变。
Register
注册时应该首先根据实现类型生成工厂函数,再把工厂函数加到服务类型对应的列表中。
Resolve
解决时应该根据服务类型找到工厂函数,然后执行工厂函数返回实例。
改进后的容器
这个容器新增了一个ResolveMany
函数,用于解决多个实例。
另外还用了Expression.Lambda
编译工厂函数,生成效率会比Activator.CreateInstance
快数十倍。
public class Container
{
private IDictionary, IList>> Factories { get; set; }
public Container()
{
Factories = new Dictionary, IList>>();
}
public void Register(string serviceKey = null)
where TImplementation : TService
{
var key = Tuple.Create(typeof(TService), serviceKey);
IList> factories;
if (!Factories.TryGetValue(key, out factories))
{
factories = new List>();
Factories[key] = factories;
}
var factory = Expression.Lambda>(Expression.New(typeof(TImplementation))).Compile();
factories.Add(factory);
}
public TService Resolve(string serviceKey = null)
{
var key = Tuple.Create(typeof(TService), serviceKey);
var factory = Factories[key].Single();
return (TService)factory();
}
public IEnumerable ResolveMany(string serviceKey = null)
{
var key = Tuple.Create(typeof(TService), serviceKey);
IList> factories;
if (!Factories.TryGetValue(key, out factories))
{
yield break;
}
foreach (var factory in factories)
{
yield return (TService)factory();
}
}
}
改进后的容器仍然有以下的问题
- 没有对实例进行生命周期管理
- 没有实现构造函数注入
实现实例的单例
以下面代码为例
var logger_a = container.Resolve();
var logger_b = container.Resolve();
使用上面的容器执行这段代码时,logger_a
和logger_b
是两个不同的对象,如果想要每次Resolve
都返回同样的对象呢?
我们可以对工厂函数进行包装,借助闭包(Closure)的力量可以非常简单的实现。
private Func
添加这个函数后在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();
container.Register();
var logger = container.Resolve();
logger.Log("Example Message");
}
在这段代码中,MyLogger
构造时需要一个ILogWriter
的实例,但是这个实例我们不能直接传给它。
这样就要求容器可以自动生成ILogWriter
的实例,再传给MyLogger
以生成MyLogger
的实例。
要实现这个功能需要使用c#中的反射机制。
把上面代码中的
var factory = Expression.Lambda>(Expression.New(typeof(TImplementation))).Compile();
换成
private Func BuildFactory(Type type)
{
// 获取类型的构造函数
var constructor = type.GetConstructors().FirstOrDefault();
// 生成构造函数中的每个参数的表达式
var argumentExpressions = new List();
foreach (var parameter in constructor.GetParameters())
{
var parameterType = parameter.ParameterType;
if (parameterType.IsGenericType &&
parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
// 等于调用this.ResolveMany();
argumentExpressions.Add(Expression.Call(
Expression.Constant(this), "ResolveMany",
parameterType.GetGenericArguments(),
Expression.Constant(null, typeof(string))));
}
else
{
// 等于调用this.Resolve();
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>(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();
container.Register();
var logger = container.Resolve();
logger.Log("asdasdas");
}
public class Container
{
private IDictionary, IList>> Factories { get; set; }
public Container()
{
Factories = new Dictionary, IList>>();
}
private Func WrapFactory(Func originalFactory, bool singleton)
{
if (!singleton)
return originalFactory;
object value = null;
return () =>
{
if (value == null)
value = originalFactory();
return value;
};
}
private Func BuildFactory(Type type)
{
// 获取类型的构造函数
var constructor = type.GetConstructors().FirstOrDefault();
// 生成构造函数中的每个参数的表达式
var argumentExpressions = new List();
foreach (var parameter in constructor.GetParameters())
{
var parameterType = parameter.ParameterType;
if (parameterType.IsGenericType &&
parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
// 等于调用this.ResolveMany();
argumentExpressions.Add(Expression.Call(
Expression.Constant(this), "ResolveMany",
parameterType.GetGenericArguments(),
Expression.Constant(null, typeof(string))));
}
else
{
// 等于调用this.Resolve();
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>(newExpression).Compile();
}
public void Register(string serviceKey = null, bool singleton = false)
where TImplementation : TService
{
var key = Tuple.Create(typeof(TService), serviceKey);
IList> factories;
if (!Factories.TryGetValue(key, out factories))
{
factories = new List>();
Factories[key] = factories;
}
var factory = BuildFactory(typeof(TImplementation));
WrapFactory(factory, singleton);
factories.Add(factory);
}
public TService Resolve(string serviceKey = null)
{
var key = Tuple.Create(typeof(TService), serviceKey);
var factory = Factories[key].Single();
return (TService)factory();
}
public IEnumerable ResolveMany(string serviceKey = null)
{
var key = Tuple.Create(typeof(TService), serviceKey);
IList> 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等,而是直接使用微软提供的标准接口。
虽然具体的实现方式离我们原来越远,但是了解一下它们的原理总是有好处的。