前言
在上一篇我大致的介紹了這個系列所涉及到的知識點,在本篇我打算把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取代它。我目前寫的案例與前面系列的版本框架有很大的差異,所以有些知識點必須和大家說明,相信在接下來的一到兩篇中,就能與大伙見面,祝大伙周末愉快。本篇測試源碼請點擊這里