依賴注入(IOC)
背景介紹
在設計模式中,尤其是結構型模式很多時候解決的就是物件間的依賴關係,變依賴具體為依賴抽象。平時開發中如果發現客戶程式依賴某個或某類物件,我們常常會對他們進行一次抽象,形成抽象的抽象類、介面,這樣客戶程式就可以擺脫所依賴的具體型別。 這個過程中有個環節被忽略了------誰來選擇客戶程式需要的滿足抽象型別的具體型別呢?通過後面的介紹你會發現很多時候建立型模式可以比較優雅的解決這個問題,但另一個問題出現了,如果您設計的不是具體的業務邏輯,而是公共庫或框架程式呢,這時候你是一個‘服務方’,不是你呼叫那些構造型別,而是它們把抽象型別傳給你,怎麼鬆散地把加工好的抽象型別傳遞給客戶程式就是另一回事了。 這個情形也就是常說的“控制反轉”,IOC:Inverse of Control;框架程式與抽象型別的呼叫關係就像常說的好萊塢規則:Don’t call me,I’ll call you
示例場景
客戶程式需要一個提供System.DateTime型別當前系統時間的物件,然後根據需要僅僅把其中的年份部分提取出來,因此最初的程式碼如下:
public class TimeProvider { public DateTime CurrentDate { get { return DateTime.Now; } } } public class Client { public int GetYear() { TimeProvider timeprovider = new TimeProvider(); return timeprovider.CurrentDate.Year; } }
後來某種原因,發現使用.NET Framework自帶的日期型別精度不夠,需要提供其他來源的TimeProvider,確保在不同精度要求的功能模組中使用不同的TimeProvider。這樣問題集中在TimeProvider的變化會影響客戶程式,但其實客戶程式僅需要抽象地使用其獲取當前時間的方法。為此,增加一個抽象介面ITimeProvider,改造後的示例如下:
public interface ITimeProvider { public DateTime CurrentDate { get ; } } public class TimeProvider:ITimeProvider { public DateTime CurrentDate { get { return DateTime.Now; } } } public class Client { public int GetYear() { ITimeProvider timeprovider = new TimeProvider(); return timeprovider.CurrentDate.Year; } }
這樣看上去客戶程式後續處理全部依賴於抽象的ITimeProvider就可以了,那麼問題是否解決了呢?沒有,因為客戶程式還要知道SystemTimeProvider的存在。因此,需要增加一個物件,由它選擇某種方式把ITimeProvider例項傳遞給客戶程式,這個物件被稱為Assembler.
對於依賴注入而言,Assembler的作用很關鍵,因為它解決了客戶程式(也就是注入型別)與待注入實體型別間的依賴關係,從此Client只需要依賴ITimeProvider和Assembler即可,它並不知道TimeProviderImpl的存在。
Assembler的職責如下:
- 知道每個具體的TimeProviderImpl的型別。
- 根據客戶程式的需要,將物件ITimeProvider反饋給客戶程式。
- 負責對TimeProviderImpl例項化。
下面是一個Assembler的示例實現:
public class Assembler {
//儲存“抽象型別/實體型別"對應關係的字典
static Dictionary<Type, Type> dictionary = new Dictionary<Type, Type>();
static Assembler() {
//註冊抽象型別需要使用的實體型別
//實際配置資訊可以從外層機制獲得,例如通過配置定義
dictionary.Add(typeof(ITimeProvider), typeof(SystemTimeProvider));
}
/// <summary> /// 根據客戶程式需要的抽象型別選擇相應的實體型別,並返回型別的例項
/// </summary>
/// <param name="type"></param>
/// <returns>實體型別例項</returns>
public object Create(Type type)//主要用於非泛型方式呼叫
{
if ((type == null) || !dictionary.ContainsKey(type))
throw new NullReferenceException();
return Activator.CreateInstance(dictionary[type]);
}
/// <summary> ///
/// </summary> /// <typeparam name="T">抽象型別(抽象類/介面/或者某種基類)</typeparam>
/// <returns></returns>
public T Create<T>()//主要用於泛型方式呼叫
{
return (T)Create(typeof(T)); }
}
}
構造注入(Constructor)
構造注入方式又稱“構造子注入”、“建構函式注入”,顧名思義,這種注入方式就是在建構函式的執行過程中,通過Assembler或其它機制把抽象型別作為引數傳遞給客戶型別。這種方式雖然相對其它方式有些粗糙,而且僅在構造過程中通過“一錘子買賣”的方式設定好,但很多時候我們設計上正好就需要這種“一次性”的注入方式。
其實現方式如下:
//在建構函式中注入 public class Client { ITimeProvider timerprovider; public Client(ITimeProvider timeProvider) { this.timerprovider = timeProvider; } }UnitTest [TestClass] public class TestClent { [TestMethod] public void TestMethod1() { ITimeProvider timeProvider = (new Assembler()).Create<ITimeProvider>(); Assert.IsNotNull(timeProvider);//確認可以正常獲得抽象型別例項 Client client = new Client(timeProvider);//在建構函式中注入 } }
設值注入(Setter)
設值注入是通過屬性方法賦值的辦法實現的。相對於構造方式而言,設值注入給了客戶型別後續修改的機會,它比較適合於客戶型別例項存活時間較長的情景。
實現方式如下:
//通過Setter實現中注入 public class Client { public ITimeProvider TimeProvider { get; set; } }
Unit Test
[TestClass] public class TestClent { [TestMethod] public void TestMethod1() { ITimeProvider timeProvider = (new Assembler()).Create<ITimeProvider>(); Assert.IsNotNull(timeProvider);//確認可以正常獲得抽象型別例項 Client client = timeProvider;//通過Setter實現注入 } }
從C#語言發展看,設定注入方式更”Lamada化“,使用時可以根據現場環境需要動態裝配,因此在新專案中我更傾向於使用設定注入。這個例子更時髦的寫法如下:
[TestClass] public class TestClent { [TestMethod] public void TestMethod1() { var clent = new Client() { TimeProvider = (new Assembler()).Create<ITimeProvider>() }; } }