一,依赖注入(Dependency Injection,简称DI)
设计模式中,尤其是结构型模式很多时候解决的就是对象间的依赖关系,变依赖关系具体为依赖具象。平时开发中如果发现客户程序依赖某个(或某类)对象,我们常常会对它们进行一次抽象,形成抽象的抽象类、接口,这样客户程序就可以摆脱所依赖的具体类型。
二,实现(C#)
示例情景
假设,现在程序需要一个获取不同时间格式的的当前时间。
我们定义一个接口ITimeProvider。
public interface ITimeProvider
{
DateTime CurrentDate { get; }
}
ITimeProvider的不同实现,提供不同的时间格式。
public class SystemTimeProvider : ITimeProvider
{
public DateTime CurrentDate { get { return DateTime.Now; } }
}
public class UtcNowTimeProvider : ITimeProvider
{
public DateTime CurrentDate { get { return DateTime.UtcNow; } }
}
需要一个装配工Assembler将所需的类型名称和实体类型一一对应。
public class Assembler
{
/// <summary>
/// 保存“类型名称/实体类型”对应关系的字典
/// </summary>
private static Dictionary<string, Type> dictionary = new Dictionary<string, Type>();
// 实际的配置信息可以从外层机制获得,例如通过xml文件配置.
static Assembler()
{
dictionary.Add("SystemTimeProvider", typeof(SystemTimeProvider));
dictionary.Add("UtcNowTimeProvider", typeof(UtcNowTimeProvider));
}
static void RegisterType(string name,Type type)
{
if ((type == null) || dictionary.ContainsKey(name)) throw new NullReferenceException();
dictionary.Add(name, type);
}
static void Remove(string name)
{
if (string.IsNullOrEmpty(name)) throw new NullReferenceException();
dictionary.Remove(name);
}
/// <summary>
/// 根据程序需要的类型名称选择相应的实体类型,并返回类型实例
/// </summary>
public ITimeProvider Create(string type)
{
if ((type == null) || !dictionary.ContainsKey(type)) throw new NullReferenceException();
Type targetType = dictionary[type];
return (ITimeProvider)Activator.CreateInstance(targetType);
}
}
此时出现一个问题,程序如何获取所需的时间格式的实现类呢?通过注入的方式。
注入的方式有以下几种。
构造函数(Constructor)注入
构造函数注入,顾名思义,就是在构造函数的时候,通过Assembler或其它机制把抽象类型作为返回给所需的程序。
/// <summary>
/// 在构造函数中注入
/// </summary>
class Client
{
private ITimeProvider timeProvider;
public Client(ITimeProvider timeProvider)
{
this.timeProvider = timeProvider;
}
}
class ConstructorInjectionTest
{
public static void Test()
{
ITimeProvider timeProvider = (new Assembler()).Create("SystemTimeProvider");
Client client = new Client(timeProvider); // 在构造函数中注入
}
}
Setter注入
Setter注入是通过属性复制的方法解决的,由于Java等很多语言中没有真正的属性,所以Setter注入一般通过一个set()方法实现,C#语言由于本身就有可写属性,所以实现起来更简洁,更像Setter。想比较Constructor方式而言,Setter给了客户类型后续修改的机会,它比较适合客户类型实例存活时间较长,但Assembler修改抽象类型指定的具体类型相对较快的情景;不过也可以由客户程序根据需要动态设置所需的类型。
/// <summary>
/// 通过Setter实现注入
/// </summary>
class Client
{
private ITimeProvider timeProvider;
public ITimeProvider TimeProvider
{
get { return this.timeProvider; } // getter本身和Setter方式实现注入没有关系
set { this.timeProvider = value; }
}
}
class SetterInjectionTest
{
public static void Test()
{
ITimeProvider timeProvider = (new Assembler()).Create("SystemTimeProvider");
Client client = new Client();
client.TimeProvider = timeProvider; // 通过Setter实现注入
}
}
接口注入
接口注入是将包括抽象类型注入的入口以方法的形式定义在一个借口里,如果客户类型需要实现这个注入过程,则实现这个接口,客户类型自己考虑如何把抽象类型引入内部。实际上接口注入有很强的侵入性,除了要求客户类型增加需要的Setter或Constructor注入的代码外,还显示地定义了一个新的接口并要求客户类型实现它。除非还有更外层容器使用的要求,或者有完善的配置系统,可以通过反射动态实现接口注入方式注入,否则并不建议采用接口注入方式。
/// <summary>
/// 定义需要注入ITimeProvider的类型
/// </summary>
interface IObjectWithTimeProvider
{
ITimeProvider TimeProvider { get; set; }
}
/// <summary>
/// 通过接口方式注入
/// </summary>
class Client : IObjectWithTimeProvider
{
private ITimeProvider timeProvider;
/// <summary>
/// IObjectWithTimeProvider Members
/// </summary>
public ITimeProvider TimeProvider
{
get { return this.timeProvider; }
set { this.timeProvider = value; }
}
}
class InterfacerInjectionTest
{
public static void Test()
{
ITimeProvider timeProvider = (new Assembler()).Create("SystemTimeProvider");
IObjectWithTimeProvider objectWithTimeProvider = new Client();
objectWithTimeProvider.TimeProvider = timeProvider; // 通过接口方式注入
}
}
基于Attribute实现注入
可以通过Attribute将附加的内容注入到对象上。
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
sealed class DecoratorAttribute : Attribute
{
public readonly object Injector;
private Type type;
public DecoratorAttribute(Type type)
{
if (type == null) throw new ArgumentNullException("type");
this.type = type;
Injector = (new Assembler()).Create("SystemTimeProvider");
}
public Type Type { get { return this.type; } }
}
/// <summary>
/// 用户帮助客户类型和客户程序获取其Attribute定义中需要的抽象类型实例
/// </summary>
static class AttributeHelper
{
public static T Injector<T>(object target)
where T : class
{
if (target == null) throw new ArgumentNullException("target");
Type targetType = target.GetType();
object[] attributes = targetType.GetCustomAttributes(typeof(DecoratorAttribute), false);
if ((attributes == null) || (attributes.Length <= 0)) return null;
foreach (DecoratorAttribute attribute in (DecoratorAttribute[])attributes)
if (attribute.Type == typeof(T))
return (T)attribute.Injector;
return null;
}
}
[Decorator(typeof(ITimeProvider))]
class Client
{
public string GetTime()
{
// 与其他方式注入不同的是,这里使用的ITimeProvider来自自己的Attribute
ITimeProvider provider = AttributeHelper.Injector<ITimeProvider>(this);
return provider.CurrentDate.ToString();
}
}
class AttributerInjectionTest
{
public static void Test()
{
Client client = new Client();
string time = client.GetTime();
}
}
示例代码
:
https://github.com/BinGithub2015/DependencyInjectionDemo
三,小结
依赖注入各种实现方式对比:
构造函数(Constructor)注入方式
:它的注入是一次性的,当客户类型构造的时候就确定了。它很适合那种生命周期不长的对象,比如在其存续期间不需要重启适配的对象。另外,相对Setter方式而言,在实现上Constructor可以节省很多代码。
Setter方式
:一个很灵活的实现方式,对于生命周期较长的客户对象而言,可以在运行过程中随时适配。
接口方式
:作为注入方式具有侵入性,很多成都上它适合需要同时约束一批客户类型的情况。
属性方式
:本身具有范围较小的固定内容侵入性(一个Attribute),它适合需要同时约束一批客户类型的情景。它本身实现相对复杂一些,但客户类型使用的时候非常方便,打标签即可。
四,参考资料
《设计模式:基于C#的工程化实现及扩展》