http://qing.weibo.com/tj/400082fa33001h7x.html
1.5 實現依賴注入1.5.1 背景介紹
設計模式中,尤其是結構型模式很多時候解決的就是對象間的依賴關系,變依賴具體為依賴抽象。平時開發中如果發現客戶程序依賴某個(或某類)對象,我們常常會對它們進行一次抽象,形成抽象的抽象類、接口,這樣客戶程序就可以擺脫所依賴的具體類型。
這個過程中有個環節被忽略了——誰來選擇客戶程序需要的滿足抽象類型的具體類型呢?通過后面的介紹你會發現很多時候創建型模式可以比較優雅地解決這個問題。但另一問題出現了,如果您設計的不是具體業務邏輯,而是公共庫或框架程序,這時候您是一個“服務方”,不是您調用那些構造類型,而是它們把抽象類型傳給您,怎么松散地把加工好的抽象類型傳遞給客戶程序就是另一回事了。
這個情形也就是常說的“控制反轉”,IOC:Inverse of Control;框架程序與抽象類型的調用關系就像常說的好萊塢規則:Don’t call me, I’ll call you.
參考Martin Fowler在《Inversion of Control Containers and the Dependency Injection pattern》一文,我們可以采用“依賴注入”的方式將加工好的抽象類型實例“注入”到客戶程序中,本書的示例也將大量采用這種方式將各種依賴項“注入”到模式實現的外部——客戶程序。下面我們結合一個具體的示例看看為什么需要依賴注入,以及Martin Fowler文中提到的三種經典方式,然后依據C#語言的特質,再擴展出一個基於Attribter方式注入(參考原有的Setter命名,這里將基於Attribute的方法稱為Attributer)。
1.5.2 示例情景客戶程序需要一個提供System.DateTime類型當前系統時間的對象,然后根據需要僅僅把其中的年份部分提取出來,因此最初的實現代碼如下:
C#
using System;
using System.Diagnostics;
namespace MarvellousWorks.PracticalPattern.Concept.DependencyInjection.Example1
{
class TimeProvider
{
public DateTime CurrentDate { get { return DateTime.Now; } }
}
public class Client
{
public int GetYear()
{
TimeProvider timeProvier = new TimeProvider();
return timeProvier.CurrentDate.Year;
}
}
}
后來因為某種原因,發現使用.NET Framework自帶的日期類型精度不夠,需要提供其他來源的TimeProvider,確保在不同精度要求的功能模塊中使用不同的TimeProvider。這樣問題集中在TimeProvider的變化會影響客戶程序,但其實客戶程序僅需要抽象地使用其獲取當前時間的方法。為此,增加一個抽象接口,確保客戶程序僅依賴這個接口ITimeProvider,由於這部分客戶程序僅需要精確到年,因此它可以使用一個名為SystemTimeProvider (:ITimeProvider)的類型。新的實現代碼如下:
C#
using System;
namespace
MarvellousWorks.PracticalPattern.Concept.DependencyInjection.Example2
{
interface ITimeProvider
{
DateTime CurrentDate { get;}
}
class TimeProvider : ITimeProvider
{
public DateTime CurrentDate { get { return DateTime.Now; } }
}
public class Client
{
public int GetYear()
{
ITimeProvider timeProvier = new TimeProvider();
return timeProvier.CurrentDate.Year;
}
}
}
這樣看上去客戶程序后續處理權都依賴於抽象的ITimeProvider,問題似乎解決了?沒有,它還要知道具體的SystemTimeProvider。因此,需要增加一個對象,由它選擇某種方式把ITimeProvider實例傳遞給客戶程序,這個對象被稱為Assembler。新的結構如圖1-19所示。
圖1-19 增建裝配對象后新的依賴關系
其中,Assembler的職責如下:
l 知道每個具體TimeProviderImpl的類型。
l 可根據客戶程序的需要,將抽象ITimeProvider反饋給客戶程序。
l 本身還負責對TimeProviderImpl的創建。
下面是一個Assembler的示例實現:
C#
public class Assembler
{
/// <summary>
/// 保存“抽象類型/實體類型”對應關系的字典
/// </summary>
private static Dictionary<Type, Type> dictionary =
new Dictionary<Type, Type>();
static Assembler()
{
// 注冊抽象類型需要使用的實體類型
// 實際的配置信息可以從外層機制獲得,例如通過配置定義
dictionary.Add(typeof(ITimeProvider), typeof(SystemTimeProvider));
}
/// 根據客戶程序需要的抽象類型選擇相應的實體類型,並返回類型實例
/// <returns>實體類型實例</returns>
public object Create(Type type) // 主要用於非泛型方式調用
{
if ((type == null) || !dictionary.ContainsKey(type)) throw new
NullReferenceException();
Type targetType = dictionary[type];
return Activator.CreateInstance(targetType);
}
/// <typeparam name="T">抽象類型(抽象類/接口/或者某種基類)</typeparam>
public T Create<T>() // 主要用於泛型方式調用
{
return (T)Create(typeof(T));
}
}
構造函數注入,顧名思義,就是在構造函數的時候,通過Assembler或其他機制把抽象類型作為參數傳遞給客戶類型。這種方式雖然相對其他方式有些粗糙,而且僅在構造過程中通過“一錘子”方式設置好,但很多時候我們設計上正好就需要這種Read Only的注入方式。其實現方式如下:
C#
/// 在構造函數中注入
class Client
{
private ITimeProvider timeProvider;
public Client(ITimeProvider timeProvider)
{
this.timeProvider = timeProvider;
}
}
Unit Test
[TestClass]
public class TestClient
{
[TestMethod]
public void Test()
{
ITimeProvider timeProvider =
(new Assembler()).Create<ITimeProvider>();
Assert.IsNotNull(timeProvider); // 確認可以正常獲得抽象類型實例
Client client = new Client(timeProvider); // 在構造函數中注入
}
}
Setter注入是通過屬性賦值的辦法解決的,由於Java等很多語言中沒有真正的屬性,所以Setter注入一般通過一個set()方法實現,C#語言由於本身就有可寫屬性,所以實現起來更簡潔,更像Setter。相比較Constructor方式而言,Setter給了客戶類型后續修改的機會,它比較適應於客戶類型實例存活時間較長,但Assembler修改抽象類型指定的具體實體類型相對較快的情景;不過也可以由客戶程序根據需要動態設置所需的類型。其實現方式如下:
C#
/// 通過Setter實現注入
class Client
{
private ITimeProvider timeProvider;
public ITimeProvider TimeProvider
{
get { return this.timeProvider; } // getter本身和以Setter方式實現
注入沒有關系
set { this.timeProvider = value; } // Setter
}
}
Unit Test
[TestClass]
public class TestClient
{
[TestMethod]
public void Test()
{
ITimeProvider timeProvider =
(new Assembler()).Create<ITimeProvider>();
Assert.IsNotNull(timeProvider); // 確認可以正常獲得抽象類型實例
Client client = new Client();
client.TimeProvider = timeProvider; // 通過Setter實現注入
}
}
接口注入是將包括抽象類型注入的入口以方法的形式定義在一個接口里,如果客戶類型需要實現這個注入過程,則實現這個接口,客戶類型自己考慮如何把抽象類型“引入”內部。實際上接口注入有很強的侵入性,除了要求客戶類型增加需要的Setter或Constructor注入的代碼外,還顯式地定義了一個新的接口並要求客戶類型實現它。除非還有更外層容器使用的要求,或者有完善的配置系統,可以通過反射動態實現接口方式注入,否則筆者並不建議采用接口注入方式。
既然Martin Fowler文中提到了這個實現方式,就給出如下示例:
C#
/// 定義需要注入ITimeProvider的類型
interface IObjectWithTimeProvider
{
ITimeProvider TimeProvider { get;set;}
}
/// 通過接口方式注入
class Client : IObjectWithTimeProvider
{
private ITimeProvider timeProvider;
/// IObjectWithTimeProvider Members
public ITimeProvider TimeProvider
{
get { return this.timeProvider; }
set { this.timeProvider = value; }
}
}
Unit Test
[TestClass]
public class TestClient
{
[TestMethod]
public void Test()
{
ITimeProvider timeProvider = (new
Assembler()).Create<ITimeProvider>();
Assert.IsNotNull(timeProvider); // 確認可以正常獲得抽象類型實例
IObjectWithTimeProvider objectWithTimeProvider = new Client();
objectWithTimeProvider.TimeProvider = timeProvider; // 通過接口方式注入
}
}
如果做個歸納,Martin Fowler之前所介紹的三種模式都是在對象部分進行擴展的,隨着語言的發展(.NET從1.0開始,Java從5開始),很多在類元數據層次擴展的機制相繼出現,比如C#可以通過Attribute將附加的內容注入到對象上。直觀上的客戶對象有可能在使用上做出讓步以適應這種變化,但這違背了依賴注入的初衷,三個角色(客戶對象、Assembler、抽象類型)之中兩個不能變,那只好在Assembler上下功夫,誰叫它的職責就是負責組裝呢?
為了實現上的簡潔,上面三個經典實現方式實際將抽象對象注入到客戶類型都是在客戶程序中(也就是那三個Unit Test部分)完成的,其實同樣可以把這個工作交給Assembler完成;而對於Attribute方式注入,最簡單的方式則是直接把實現了抽象類型的Attribute定義在客戶類型上,例如:
C#
(錯誤的實現情況)
class SystemTimeAttribute : Attribute, ITimeProvider { … }
[SystemTime]
class Client { … }
相信您也發現了,這樣雖然把客戶類型需要的ITimeProvider通過“貼標簽”的方式告訴它了,但事實上又把客戶程序與SystemTimeAttribute“綁”上了,它們緊密地耦合在一起。參考上面的三個實現,當抽象類型與客戶對象耦合的時候我們引入了Assembler,當Attribute方式出現類似的情況時,我們寫個AttributeAssembler不就行了么?還不行。設計上要把Attribute設計成一個“通道”,考慮到擴展和通用性,它本身要協助AttributeAssembler完成ITimeProvider的裝配,最好還可以同時裝載其他抽象類型來修飾客戶類型。示例代碼如下:
C#
[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(this.type);
}
/// 客戶類型需要的抽象對象類型
public Type Type { get { return this.type; } }
}
/// 幫助客戶類型和客戶程序獲取其Attribute定義中需要的抽象類型實例的工具類
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))]
// 應用Attribute,定義需要將ITimeProvider通過它注入
class Client
{
public int GetYear()
{
// 與其他方式注入不同的是,這里使用的ITimeProvider來自自己的Attribute
ITimeProvider provider =
AttributeHelper.Injector<ITimeProvider>(this);
return provider.CurrentDate.Year;
}
}
Unit Test
[TestMethod]
public void Test()
{
Client client = new Client();
Assert.IsTrue(client.GetYear() > 0);
}
依賴注入雖然被Martin Fowler稱為一個模式,但平時使用中,它更多地作為一項實現技巧出現,開發中很多時候需要借助這項技巧把各個設計模式所加工的成果傳遞給客戶程序。各種實現方式雖然最終目標一致,但在使用特性上有很多區別。
l Constructor方式:它的注入是一次性的,當客戶類型構造的時候就確定了。它很適合那種生命期不長的對象,比如在其存續期間不需要重新適配的對象。此外,相對Setter方式而言,在實現上Constructor可以節省很多代碼;
l Setter方式:一個很靈活的實現方式,對於生命期較長的客戶對象而言,可以在運行過程中隨時適配;
l 接口方式:作為注入方式具有侵入性,很大程度上它適於需要同時約束一批客戶類型的情況;
l 屬性方式:隨着開發語言的發展引入的新方式,它本身具有范圍較小的固定內容侵入性(一個DecoratorAttribute),它也很適合需要同時約束一批客戶類型情景。它本身實現相對復雜一些,但客戶類型使用的時候非常方便——“打標簽”即可。