前言
在上一篇我大致的介绍了这个系列所涉及到的知识点,在本篇我打算把IOC这一块单独提取出来讲,因为IOC容器在解除框架层与层之间的耦合有着不可磨灭的作用。当然在本系列前面的三篇中我也提供了一种基于反射的解耦方式,但是始终不是很优雅,运用到项目中显得别扭。目前,我所掌握的IOC容器主要有两个:一个是 unity,另一个则是spring.net,经过慎重的思考我还是决定选择unity 2.0做为本系列的IOC容器,原因主要有两个:第一,他是一个轻量级的容器且师出名门(微软),第二,它提供了简单的拦截机制,在它的基础上实现AOP显得非常的简单,下面开始我们今天的议题......
什么是IOC容器
IOC容器是对控制反转与依赖注入的一种实现,关于什么是控制反转,什么是依赖注入,网上一搜一大把,我这里就不在多说了,我们需要关注的就是IOC容器到底能够为我们做些什么事情,其实说白了,IOC容器就是通过相应的配置,用来为我们创建实例,使我们摆脱了new的魔咒,这在层与层之间的解耦中有着重要的意义,至于层次间为什么要解耦请参见我的第一篇, 本文着重介绍unity 2.0,您需要在项目中添加对Microsoft.Practices.Unity.dll与Microsoft.Practices.Unity.Configuration.dll的引用,下面我通过简单doom来讲述它的运用,程序如图
IOC项目引用了IService项目,但并未引用service项目,IService项目中定义的是服务接口,Service项目引用了IService项目并实现了里面的服务接口。我们现在要做的事情就是在IOC中采用IService接口标识服务,在调用时采用unity容器读取配置文件帮助我们把接口实例化,其具体的服务来自Service项目(我们的IOC项目没有引用Service项目所以是无法new的),为了很好的运用Unity容器,我做了一下封装,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using Microsoft.Practices.Unity; using Microsoft.Practices.Unity.Configuration; using System.Configuration; using System.Reflection; namespace IOC { public class ServiceLocator { /// <summary> /// IOC容器 /// </summary> private readonly IUnityContainer container; private static readonly ServiceLocator instance = new ServiceLocator(); /// <summary> /// 服务定位器单例 /// </summary> public static ServiceLocator Instance { get { return instance; } } private ServiceLocator() { //读取容器配置文件 UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity"); //创建容器 container = new UnityContainer(); //配置容器 section.Configure(container); } #region /// <summary> /// 创建构造函数参数 /// </summary> /// <param name="overridedArguments"></param> /// <returns></returns> private IEnumerable<ParameterOverride> GetParameterOverrides(object overridedArguments) { List<ParameterOverride> overrides = new List<ParameterOverride>(); Type argumentsType = overridedArguments.GetType(); argumentsType.GetProperties(BindingFlags.Public | BindingFlags.Instance) .ToList() .ForEach(property => { var propertyValue = property.GetValue(overridedArguments, null); var propertyName = property.Name; overrides.Add(new ParameterOverride(propertyName, propertyValue)); }); return overrides; } #endregion #region 公共方法 /// <summary> /// 创建指定类型的容器 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public T GetService<T>() { return container.Resolve<T>(); } /// <summary> /// 根据指定名称的注册类型 /// 创建指定的类型 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="name">注册类型配置节点名称</param> /// <returns></returns> public T GetService<T>(string name) { return container.Resolve<T>(name); } /// <summary> /// 用指定的构造函数参数 /// 创建实体 /// </summary> /// <typeparam name="T">实体类型</typeparam> /// <param name="overridedArguments">属性名对应参数名,属性值对应 /// 参数值得动态参数实体</param> /// <returns></returns> public T GetService<T>(object overridedArguments) { var overrides = GetParameterOverrides(overridedArguments); return container.Resolve<T>(overrides.ToArray()); } /// <summary> /// /// </summary> /// <typeparam name="T"></typeparam> /// <param name="name"></param> /// <param name="overridedArguments"></param> /// <returns></returns> public T GetService<T>(string name,object overridedArguments) { var overrides = GetParameterOverrides(overridedArguments); return container.Resolve<T>(name, overrides.ToArray()); } #endregion } }
好了,下面开始我们的测试,我们首先在IService项目创建一个ISayHello服务接口代码如下
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace IService { public interface ISayHello { string hello(); } }
下面我们在Service项目中创建一个ChineseSayHello服务实现ISayHello接口代码如下
using System; using System.Collections.Generic; using System.Linq; using System.Text; using IService; namespace Service { public class ChineseSayHello : ISayHello { public string hello() { return "你好"; } } }
下面我们创建一个测试页面Test.aspx,后台代码如下
using IService; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace IOC { public partial class Test : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { ISayHello sayhello = ServiceLocator.Instance.GetService<ISayHello>(); showInfo.InnerText = sayhello.hello(); } } }
好,下面来看一看我们的配置文件
<?xml version="1.0" encoding="utf-8"?> <!-- 有关如何配置 ASP.NET 应用程序的详细消息,请访问 http://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <configSections> <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/> </configSections> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <container> <register type="IService.ISayHello,IService" mapTo="Service.ChineseSayHello,Service"> </register> </container> </unity> <system.web> <compilation debug="true" targetFramework="4.0" /> </system.web> </configuration>
<register/>节点是告诉容器我要向容器中注册一个ISayHello接口类型,并且当每次要创建的ISayHello类型的时候都映射到ChineseSayHello实例。我们执行程序,得到的结果为:你好,这说明我们的容器正确的为我们创建ChineseSayHello实例。如果有一天我们觉得ChineseSayHello不好,我们想换一个服务来实现ISayHello,比如:EnglishSayHello,从而替代ChineseSayHello,我们仅需要创建一个EnglishSayHello类型,修改下配置文件,如下
using System; using System.Collections.Generic; using System.Linq; using System.Text; using IService; namespace Service { public class EnglishSayHello : ISayHello { public string hello() { return "hello"; } } }
<?xml version="1.0" encoding="utf-8"?> <!-- 有关如何配置 ASP.NET 应用程序的详细消息,请访问 http://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <configSections> <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/> </configSections> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <container> <!--<register type="IService.ISayHello,IService" mapTo="Service.ChineseSayHello,Service"> </register>--> <register type="IService.ISayHello,IService" mapTo="Service.EnglishSayHello,Service"> </register> </container> </unity> <system.web> <compilation debug="true" targetFramework="4.0" /> </system.web> </configuration>
程序的运行结果为:hello,实例创建成功。简简单单的一个例证,我们看见了IOC容器在给我们带来的巨大好处,我们IOC层根本不再依赖于具体的服务,我们想要什么实例配置下文件即可,这样极大的增加了程序的灵活性与可扩张性.
下面,我们来讨论一下容器实例的生命周期,也就是实例在容器中的存活时间。举个例子,我们在同样的配置文件下连续创建多个ISayHello服务实例,很显然,这样的多个实例是来自同样的类型的,现在我们关心的是容器是每一次都会为我们创建该类型的实例,还是仅仅只为我们创建一个,以后所有的ISayHello都引用同一个实例呢?我们测试下,代码如下
using IService; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace IOC { public partial class Test : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { ISayHello sayhello = ServiceLocator.Instance.GetService<ISayHello>(); ISayHello sayhello1 = ServiceLocator.Instance.GetService<ISayHello>(); showInfo.InnerText = sayhello.GetHashCode().Equals(sayhello1.GetHashCode()).ToString(); } } }
我们得到的结果是False,很显然容器每次都为我们去创建了一个实例。事实上Unity容器创建实例的机制是这样的:首先去容器中查找有没有这样的实例还保持在容器中,如果有的话则直接拿出来,如果没有的话则重新去创建一个。现在关键的问题是容器采用什么用的机制去保存这些被创建出来的实例,也就是实例在容器中的生命周期,在默认的情况下,实例被创建出来,容器即不再保存该实例,故在下次创建的时候容器找不到这样的实例,从而重新创建该类型实例,事实上实例的生命周期是可以配置的,我们甚至可以自定义实例的生命周期,下面我们修改下配置文件,设置实例的lifetime类型为singleton,即让实例永远保持在容器中,如下
<?xml version="1.0" encoding="utf-8"?> <!-- 有关如何配置 ASP.NET 应用程序的详细消息,请访问 http://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <configSections> <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/> </configSections> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <container> <!--<register type="IService.ISayHello,IService" mapTo="Service.ChineseSayHello,Service"> </register>--> <register type="IService.ISayHello,IService" mapTo="Service.EnglishSayHello,Service"> <lifetime type="singleton"/> </register> </container> </unity> <system.web> <compilation debug="true" targetFramework="4.0" /> </system.web> </configuration>
我们在运行程序,得到的结果是:True,说明我们每次都引用了同一个实例,容器很好的帮我们实现了单例模式,除了singleton外,容器还默认了其他的几种实例生命周期,这里就不在多说了。注:我们所说的实例生命周期不是指实例的创建到销毁,而是指实例在容器中创建,受容器管辖的时间范围。
Unity容器支持为一个接口或者基类注册多个映射节点,但是每个节点需要采用不同的名称标识,在创建实例的时候,也通过该节点名称来创建指定的映射实例,例如
<?xml version="1.0" encoding="utf-8"?> <!-- 有关如何配置 ASP.NET 应用程序的详细消息,请访问 http://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <configSections> <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/> </configSections> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <container> <register type="IService.ISayHello,IService" mapTo="Service.ChineseSayHello,Service"> </register> <register name="english" type="IService.ISayHello,IService" mapTo="Service.EnglishSayHello,Service"> </register> </container> </unity> <system.web> <compilation debug="true" targetFramework="4.0" /> </system.web> </configuration>
using IService; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace IOC { public partial class Test : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { ISayHello sayhello = ServiceLocator.Instance.GetService<ISayHello>(); ISayHello sayhello1 = ServiceLocator.Instance.GetService<ISayHello>("english"); showInfo.InnerText = string.Format("{0}+{1}", sayhello1.hello(), sayhello.hello()); //showInfo.InnerText = sayhello.GetHashCode().Equals(sayhello1.GetHashCode()).ToString(); } } }
结果为:hello+你好,我们成功的通过了配置文件中的注册节点名称来创建我们的具体服务实例。我们知道创建实例是需要调用实例的构造函数的,很显然容器默认的为我们调用了构造函数,倘若构造函数带有参数,则容器则会创建相应的参数实例。现在问题来了,假如我的参数是一个接口或者抽象类型怎么办? 很显然要能创建这样的参数我们就必须知道参数的映射类型, 看如下例子,我们在IService项目中重新创建一个接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace IService
{
public interface ISay
{
string Say();
}
}
我们写一个服务实现该接口
using System; using System.Collections.Generic; using System.Linq; using System.Text; using IService; namespace Service { public class ComSayHello_V2 : ISay { public ISayHello Chinese { get; set; } public ComSayHello_V2(ISayHello chinese) { this.Chinese = chinese; } public string Say() { return this.Chinese.hello(); } } }
配置文件如下
<?xml version="1.0" encoding="utf-8"?> <!-- 有关如何配置 ASP.NET 应用程序的详细消息,请访问 http://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <configSections> <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/> </configSections> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <container> <register type="IService.ISayHello,IService" mapTo="Service.ChineseSayHello,Service"> </register> <register name="english" type="IService.ISayHello,IService" mapTo="Service.EnglishSayHello,Service"> </register> <register type="IService.ISay,IService" mapTo="Service.ComSayHello_V2,Service"> </register> </container> </unity> <system.web> <compilation debug="true" targetFramework="4.0" /> </system.web> </configuration>
using IService; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace IOC { public partial class Test : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { //ISayHello sayhello = ServiceLocator.Instance.GetService<ISayHello>(); //ISayHello sayhello1 = ServiceLocator.Instance.GetService<ISayHello>("english"); //showInfo.InnerText = string.Format("{0}+{1}", sayhello1.hello(), sayhello.hello()); //showInfo.InnerText = sayhello.GetHashCode().Equals(sayhello1.GetHashCode()).ToString(); ISay say = ServiceLocator.Instance.GetService<ISay>(); showInfo.InnerText=say.Say(); } } }
我们得到结果:你好。在配置文件中我们添加了两个注册节点,从结果中我们看见,容器默认选择了未命名的节点,倘若我们注释该节点程序将报错,程序没办法自动识别带名称的节点,要想让程序识别带名称的节点我们需要配置构造函数参数,配置如下
<?xml version="1.0" encoding="utf-8"?> <!-- 有关如何配置 ASP.NET 应用程序的详细消息,请访问 http://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <configSections> <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/> </configSections> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <container> <!--<register type="IService.ISayHello,IService" mapTo="Service.ChineseSayHello,Service"> </register>--> <register name="english" type="IService.ISayHello,IService" mapTo="Service.EnglishSayHello,Service"> <!--<lifetime type="singleton"/>--> </register> <register type="IService.ISay,IService" mapTo="Service.ComSayHello_V2,Service"> <constructor> <param name="say" dependencyName="english"></param> </constructor> </register> </container> </unity> <system.web> <compilation debug="true" targetFramework="4.0" /> </system.web> </configuration>
结果正确的显示为:hello,在这里顺便提一下如果我们取消english注册节点lifetime的注释,我们会发现每次创建ComSayHello_V2的参数将来自同一个实例的引用,原因请参见,上文的实例生命周期。
当然我们也可以直接在配置文件的构造函数中指定,参数类型而避免注册其他类型节点,配置文件代码如下,结果一样
<?xml version="1.0" encoding="utf-8"?> <!-- 有关如何配置 ASP.NET 应用程序的详细消息,请访问 http://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <configSections> <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/> </configSections> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <container> <register type="IService.ISay,IService" mapTo="Service.ComSayHello_V2,Service"> <constructor> <param name="say" dependencyType="Service.EnglishSayHello,Service"></param> </constructor> </register> </container> </unity> <system.web> <compilation debug="true" targetFramework="4.0" /> </system.web> </configuration>
其实,我们还能够在配置文件中给参数赋值,但是如果参数是一个复杂类型,比如类的时候,我们就需要一个转换器,把字符串类型的值转换为指定的赋值类型,因为在配置文件中我们赋值的类型只能是string。转换器在平时实践中用的少,所以我不打算多说。需要注意的是,如果我们的类中有多个构造函数的话,那么容器默认总会选择参数最多的那个构造函数。
以上所介绍的归根到底也只是一种构造函数注入。其实Unity还提供能属性注入与方法注入,即在创建实例的时候动态为某个属性赋值或者调用某个方法,其实这个要做到也蛮简单的,我们只需要在相应的属性上面打上[Dependency]特性,在方法上打上[InjectionMethod]特性即可,但是这两种方式对类的侵入性太强,不推荐使用
总结
本文简单的演示了Unity IOC的一些使用方法,因为在我的框架中,Unity在层次解耦中充当了重要的作用,除此之外Unity其实还能实现的AOP拦截,但是由于篇幅的原因不再多讲,在这里要提醒大家务必理解实体的生命周期,因为这对实现单元工作模式有着重要的意义。在我的系列前三篇中,我都是采用了是反射来解耦,有兴趣的朋友可以尝试下用Unity取代它。我目前写的案例与前面系列的版本框架有很大的差异,所以有些知识点必须和大家说明,相信在接下来的一到两篇中,就能与大伙见面,祝大伙周末愉快。本篇测试源码请点击这里