依赖注入与Unity
关于控制反转(Inversion of Control)和依赖注入(Dependency Injection)大家网上可以找下相关概念,在《小菜学习设计模式(五)—控制反转(Ioc)》这篇文章中本人也有详细的解释,这边再说明下,有很多人把控制反转和依赖注入混为一谈,虽然在某种意义上来看他们是一体的,但好像又有些不同,就比如在上篇文章中所提到的示例。控制反转(Ioc)可以看成自来水厂,那自来水厂的运行就可以看作依赖注入(DI),Ioc是一个控制容器,DI就是这个容器的运行机制,有点像国家主席和总理的意思。
构造器注入
构造器注入(Constructor Injection):IoC容器会智能地选择选择和调用适合的构造函数以创建依赖的对象。如果被选择的构造函数具有相应的参数,IoC容器在调用构造函数之前解析注册的依赖关系并自行获得相应参数对象。
通过上面的定义看以看出,使用构造器注入需要在在构造函数中传递一个抽象参数,Ioc会自动解析具象所依赖的抽象并注册给具象,我们还是用上篇喝水作为示例:
/// <summary>
/// 人接口
/// </summary>
public interface IPeople
{
void DrinkWater();
}
/// <summary>
/// 村民
/// </summary>
public class VillagePeople : IPeople
{
IWaterTool _pw;
public VillagePeople(IWaterTool pw)
{
_pw = pw;
}
public void DrinkWater()
{
Console.WriteLine(_pw.returnWater());
}
}
/// <summary>
/// 压水井
/// </summary>
public class PressWater : IWaterTool
{
public string returnWater()
{
return "地下水好甜啊!!!";
}
}
/// <summary>
/// 获取水方式接口
/// </summary>
public interface IWaterTool
{
string returnWater();
}
代码很简单,PressWater
依赖于IWaterTool
,在VillagePeople
构造函数中传递一个IWaterTool
的抽象,我们看下调用代码:
static void Main(string[] args)
{
/*
IWaterTool waterTool = new PressWater();
IPeople people = new VillagePeople(waterTool);
people.DrinkWater(); //喝水
*/
UnityContainer container = new UnityContainer(); //创建容器
container.RegisterType<IWaterTool, PressWater>(); //注册依赖对象
IPeople people = container.Resolve<VillagePeople>(); //返回调用者
people.DrinkWater(); //喝水
}
上面主要用到Unity的RegisterType
和Resolve
的泛型方法,我们看下RegisterType的方法签名:
https://github.com/unitycontainer/abstractions/blob/master/src/Utility/UnityContainerExtensions.cs
/// <summary>
/// Register a type mapping with the container.
/// </summary>
/// <remarks>
/// <para>
/// This method is used to tell the container that when asked for type <typeparamref name="TFrom"/>,
/// actually return an instance of type <typeparamref name="TTo"/>. This is very useful for
/// getting instances of interfaces.
/// </para>
/// <para>
/// This overload registers a default mapping and transient lifetime.
/// </para>
/// </remarks>
/// <typeparam name="TFrom"><see cref="Type"/> that will be requested.</typeparam>
/// <typeparam name="TTo"><see cref="Type"/> that will actually be returned.</typeparam>
/// <param name="container">Container to configure.</param>
/// <param name="injectionMembers">Injection configuration objects.</param>
/// <returns>The <see cref="Unity.IUnityContainer"/> object that this method was called on (this in C#, Me in Visual Basic).</returns>
public static IUnityContainer RegisterType<TFrom, TTo>(this IUnityContainer container, params InjectionMember[] injectionMembers) where TTo : TFrom
{
return (container ?? throw new ArgumentNullException(nameof(container))).RegisterType(typeof(TFrom), typeof(TTo), null, null, injectionMembers);
}
我们可以看到RegisterType
的第一个参数是this IUnityContainer container,我们上面调用的时候并没有传递一个IUnityContainer 类型的参数, 为什么这里会有一个this关键字,做什么用?其实这就是扩展方法。这个扩展方法在静态类中声明,定义一个静态方法(UnityContainerExtensions类和RegisterType都是静态的), 其中第一个参数定义可它的扩展类型。RegisterType方法扩展了UnityContainerExtensions类,因为它的第一个参数定义了 IUnityContainer(UnityContainerExtensions的抽象接口) 类型,为了区分扩展方法和一般的静态方法,扩展方法还需要给第一个参数使用this关键字。
还有就是RegisterType
的泛型约束 where TTo : TFrom
; TTo
必须是TFrom
的派生类,就是说TTo
依赖于TFrom
。
我们再来看下Resolve
泛型方法的签名:
//
// 摘要:
// Resolve an instance of the default requested type from the container.
// 解决从容器的默认请求的类型的实例,就是获取调用者的对象。
//
// 参数:
// container:
// Container to resolve from.
//
// overrides:
// Any overrides for the resolve call.
//
// 类型参数:
// T:
// System.Type of object to get from the container.
//
// 返回结果:
// The retrieved object.
public static T Resolve<T>(this IUnityContainer container, params ResolverOverride[] overrides);
“Resolve an instance of the default requested type from the container”,这句话可以翻译为:解决从容器的默认请求的类型的实例,就是获取调用者的对象。
关于RegisterType
和Resolve
我们可以用自来水厂的例子来说明,请看下面:
RegisterType
:可以看做是自来水厂决定用什么作为水源,可以是水库或是地下水,我只要“注册”开关一下就行了。Resolve
:可以看做是自来水厂要输送水的对象,可以是农村或是城市,我只要“控制”输出就行了。
Dependency属性注入
属性注入(Property Injection):如果需要使用到被依赖对象的某个属性,在被依赖对象被创建之后,IoC容器会自动初始化该属性。
/// <summary>
/// 其他人
/// </summary>
public class OtherPeople : IPeople
{
[Unity.Attributes.Dependency]
public IWaterTool _pw { get; set; }
public void DrinkWater()
{
Console.WriteLine(_pw.returnWater());
}
}
调用方式和构造器注入一样,通过RegisterType<IWaterTool, PressWater>();注入就可以了,除了使用RegisterType
方法注册,我们还可以在配置文件中注册,[Dependency]和RegisterType方式其实都会产生耦合度,我们要添加一个属性或是修改一中注册都会去修改代码,我们要做的就是代码不去修改,只要修改配置文件了,这个在下面有讲解,这边就不多说,我们先看下使用UnityConfigurationSection的Configure方法加载配置文件注册:
![](https://images2018.cnblogs.com/blog/196558/201803/196558-20180304014849664-352237301.png)
UnityContainer container = new UnityContainer();//创建容器
section.Configure(container, "defaultContainer");
IPeople people = container.Resolve<IPeople>();//返回调用者
people.DrinkWater();//喝水
}
# InjectionMethod方法注入 #
>方法注入(Method Injection):如果被依赖对象需要调用某个方法进行相应的初始化,在该对象创建之后,IoC容器会自动调用该方法
方法注入和属性方式使用一样,方法注入只需要在方法前加`[InjectionMethod]`标记就行了,从方法注入的定义上看,只是模糊的说对某个方法注入,并没有说明这个方法所依赖的对象注入,所依赖的对象无非就三种:参数、返回值和方法内部对象引用,我们做一个示例试下:
/// <summary>
/// 村民3
/// </summary>
public class VillagePeople3 : IPeople
{
public IWaterTool tool; //对象引用
public IWaterTool tool2; //参数
public IWaterTool tool3; //返回值
[InjectionMethod]
public void DrinkWater()
{
if (tool == null)
{}
}
[InjectionMethod]
public void DrinkWater2(IWaterTool tool2)
{
this.tool2 = tool2;
}
[InjectionMethod]
public IWaterTool DrinkWater3()
{
return tool3;
}
}
调用代码:
ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
fileMap.ExeConfigFilename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory + "config\\Unity.Config.xml");
Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
UnityConfigurationSection section =
(UnityConfigurationSection)configuration.GetSection(UnityConfigurationSection.SectionName);
UnityContainer container = new UnityContainer();//创建容器
section.Configure(container, "defaultContainer2");
VillagePeople3 people = container.Resolve<IPeople>() as VillagePeople3;//返回调用者
Console.WriteLine("people.tool == null(引用) ? {0}", people.tool == null ? "Yes" : "No");
Console.WriteLine("people.tool2 == null(参数) ? {0}", people.tool2 == null ? "Yes" : "No");
Console.WriteLine("people.tool3 == null(返回值) ? {0}", people.tool3 == null ? "Yes" : "No");
container.Resolve<IPeople>() as VillagePeople03;其实多此一举,因为已经在配置文件注册过了,不需要再进行转化,这边只是转化只是方便访问VillagePeople03对象的几个属性值,我们看下运行效果:
![](https://images2018.cnblogs.com/blog/196558/201803/196558-20180304152807090-161992613.png)
结果不言而喻,其实我们理解的方法注入就是对参数对象的注入,从`typeConfig`节点`-method`节点`-param`节点就可以看出来只有参数的配置,而并没有其他的配置,关于`typeConfig`下面会讲到。
# 非泛型注入 #
我们也可以使用非泛型注入,代码如下:
public static void FuTest04()
{
UnityContainer container = new UnityContainer();//创建容器
container.RegisterType(typeof(IWaterTool), typeof(PressWater));//注册依赖对象
IPeople people = (IPeople)container.Resolve(typeof(VillagePeople));//返回调用者
people.DrinkWater();//喝水
}
# 标识键 #
我们知道,Unity提供了对象的容器,那么这个容器是如何进行索引的呢?也就是说,容器内的单元是如何标识的呢?在Unity中,标识主要有两种方式, 一种是直接使用接口(或者基类)作为标识键,另一种是使用接口(或者基类)与名称的组合作为标识键,键对应的值就是具体类。
第一种使用接口(或者基类)作为标识键:
container.RegisterType<IWaterTool, PressWater>();
代码中的IWaterTool就是作为标识键,你可以使用`基类或是抽象类`作为标识,获取注册对象:container.Resolve<IWaterTool>(),如果一个Ioc容器容器里面注册了多个接口或是基类标示,我们再这样获取就不知道注册的是哪一个?怎么解决,就是用`接口或是基类与名称`作为标识键,示例代码如下:
public static void FuTest05()
{
UnityContainer container = new UnityContainer();//创建容器
//("WaterTool1")括号里面的name: <param name="name">将用于请求类型的名称.</param>
container.RegisterType<IWaterTool, PressWater>("WaterTool1");//注册依赖对象WaterTool1
container.RegisterType<IWaterTool, PressWater>("WaterTool2");//注册依赖对象WaterTool2
IWaterTool wt = container.Resolve<IWaterTool>("WaterTool1");//返回依赖对象WaterTool1
var list = container.ResolveAll<IWaterTool>();//返回所有注册类型为IWaterTool的对象
}
<img src="https://images0.cnblogs.com/i/435188/201404/181423367752431.jpg" alt="">
# ContainerControlledLifetimeManager单例 #
关于单例概念可以参考《[小菜学习设计模式(二)—单例(Singleton)模式](http://www.cnblogs.com/xishuai/p/3509346.html)》这篇文章,为了实现单例模式,我们通常的做法是,在类中定义一个方法如GetInstance,判断如果实例为null则新建一个实例,否则就返回已有实例。但是我觉得这种做法将对象的生命周期管理与类本身耦合在了一起。所以我觉得遇到需要使用单例的地方,应该将生命周期管理的职责转移到对象容器Ioc上,而我们的类依然是一个干净的类,使用Unity创建单例代码:
/// <summary>
/// ContainerControlledLifetimeManager 单例
/// </summary>
///
public static void FuTest07()
{
UnityContainer container = new UnityContainer();//创建容器
container.RegisterType<IWaterTool, PressWater>(new ContainerControlledLifetimeManager());//注册依赖对象
IPeople people = container.Resolve<VillagePeople>();//返回调用者
people.DrinkWater();//喝水
}
上面演示了将IWaterTool注册为PressWater,并声明为单例,`ContainerControlledLifetimeManager`字面意思上就是Ioc容器管理声明周期,我们也可以不使用类型映射,将某个类注册为单例:
container.RegisterType
除了将类型注册为单例,我们也可以将已有对象注册为单例,使用`RegisterInstance`方法,示例代码:
PressWater pw = new PressWater();
container.RegisterInstance
上面的代码就表示将PressWater的pw对象注册到Ioc容器中,并声明为单例。
如果我们在注册类型的时候没有指定ContainerControlledLifetimeManager对象,Resolve获取的对象的生命周期是短暂的,Ioc容器并不会保存获取对象的引用,就是说我们再次Resolve获取对象的时候,获取的是一个全新的对象,如果我们指定ContainerControlledLifetimeManager,类型注册后,我们再次Resolve获取的对象就是上次创建的对象,而不是再重新创建对象,这也就是单例的意思。
# Unity的app.config节点配置 #
上面所说的三种注入方式,包括单例创建都是在代码中去配置的,当然只是演示用,这种配置都会产生耦合度,比如添加一个属性注入或是方法注入都要去属性或是方法前加`[Dependency]`和`[InjectionMethod]`标记,我们想要的依赖注入应该是去配置文件中配置,当系统发生变化,我们不应去修改代码,而是在配置文件中修改,这才是真正使用依赖注入解决耦合度所达到的效果,先看下Unity完整的配置节点:
![](https://images2018.cnblogs.com/blog/196558/201803/196558-20180312193938928-1759222448.png)
上面的图大家可能都见过,我再贴一下Unity示例节点配置,原文地址:msdn.microsoft.com/en-us/library/ff647848.aspx,稍微翻译了下。
<instances>
<add name="MyInstance1" type="System.String" value="Some value" />
<add name="MyInstance2" type="System.DateTime" value="2008-02-05T17:50:00" />
</instances>
<extensions>
<add type="MyApp.MyExtensions.SpecialOne" />
</extensions>
<extensionConfig>
<add name="MyExtensionConfigHandler" type="MyApp.MyExtensions.SpecialOne.ConfigHandler" />
</extensionConfig>
</container>
根据上面的配置,我做了一个简单的方法注入示例,配置注册信息都是在文件中,并不是使用RegisterType方法进行注册,但是报下面错误:
![](https://images0.cnblogs.com/i/435188/201404/192339230572403.jpg)
无法识别的元素“typeConfig”。示例是完全按照unity配置说明进行配置的,Google或是百度都找不到问题所在,也试了很多种方式,最后Google英文“Unrecognized element 'typeConfig'”,终于找到问题:http://unity.codeplex.com/discussions/209002。英文我大概看得不是很懂,好像是unity版本问题,unity2.0配置文件中并没有typeConfig节点,而我们使用的unity程序集是最新的,而并没有识别typeConfig节点,这个问题很严重,也花了我两天时间,就像那位提问者最后所说:“I can not tell you how grateful I am, I've spent the last 2 days trying to figure this one out.”,虽然问题原因找到了,但是是通过英文搜索找到了,就是说国外遇到过类似问题,难道我们国内没有遇到过,郁闷。
大家可能有些纳闷,为什么你上面几个示例使用的unity配置没有报错,我当时在写示例的时候只是使用简单的类型映射,并不是使用完整的unity配置文件,然后就网上找了下,就找到了http://msdn.microsoft.com/en-us/library/ff647848.aspx,当时没有注意版本问题,所以就出现了上面的问题,我们上面示例unity配置文件中Register节点就是unity2.0的配置,这也是为什么上面示例可以运行的原因。
unity2.0的配置文件配置说明找了好久,终于在MSDN找到了:http://msdn.microsoft.com/en-us/library/ff660914%28v%3Dpandp.20%29.aspx#config_constructor,没有中文版本,英文不好的朋友可以使用Google简单翻译下,注意这段话“Unity 2.0 uses a new streamlined configuration schema for configuring Unity.”,这才是我们要使用的unity2.0的配置文件,其实和1.2版本差不多,只不过是简化了一些东西,配置起来也更加方便,这边就不多说了,列一下配置目录:
- The <unity> Configuration Section
- The <container> Element
- The <register> Element
- The <lifetime> Element
- The <constructor> Element
- The <property> Element
- The <method> Element
- The <param> Element
- The <dependency> Element
- The <value> Element
- The <optional> Element
- The <array> Element
- The <extension> Element
- The <instance> Element
- The <namespace > Element
- The <alias> Element
- The <sectionExtension> Element
另外在unity2.0配置中alias节点下的生命管理周期配置并不需要了,比如我们创建一个单例注册类型,只需要配置下面就可以了:
<containers>
<container name="defaultContainer">
<register type="UnityContainerDemo.IPeople, UnityContainerDemo" mapTo="UnityContainerDemo.VillagePeople01, UnityContainerDemo">
<lifetime type="singleton" />
</register>
<register type="UnityContainerDemo.IWaterTool, UnityContainerDemo" mapTo="UnityContainerDemo.PressWater, UnityContainerDemo"/>
</container>
</containers>
</unity>
并不需要再像unity1.2中创建下面节点:
<typeAlias alias="singleton" type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager,Microsoft.Practices.Unity" />
转自:Unity依赖注入使用详解 - 田园里的蟋蟀 - 博客园 http://www.cnblogs.com/xishuai/p/3670292.html