最重要的DI模式。
算法
如何工做:一個私有的只讀引用指向依賴,一個帶參的構造器初始化該引用。數據庫
Tip1: 保持構造器邏輯的簡潔,不要包含其它的邏輯。
Tip2: 能夠把構造器注入當作是靜態地聲明瞭類的依賴。明確指明瞭依劣勢賴的類型。編程
什麼時候使用:應該是你默認的DI選擇。安全
Tip1: 若是能夠將構造器設計爲單一的。重載的構造器會給DI容器形成誤導。框架
優缺點:ide
優點 | 劣勢 |
---|---|
最容易實現的DI模式 | 有些框架假定你有默認構造器,所以使用它很困難;另外一個顯見的劣勢是在程序初始化時就須要整個依賴圖,不過不用擔憂出現性能問題。 |
適用於有本地默認依賴,而且但願開發擴展的場景。
函數
如何工做: 一個可寫的屬性性能
Tip1 : 又叫作依賴設置器注入,須要爲屬性設置一個默認值。
Tip2 : 若是容許在類的聲明週期中切換依賴,能夠經過在內部引入一個flag來確保依賴只容許被設置一次。測試
什麼時候使用: 用於依賴是可選的狀況網站
Tip1 : 若是你只想留一個擴展點,那麼建議你使用Null-Object模式來實現本地默認屬性的初始化。
Tip2 : 若是你想要保留默認的屬性,還想要更多的擴展,那麼可使用觀察者模式或者組合模式。
優缺點:
優點 | 劣勢 |
---|---|
容易理解 | 健壯地實現它不太容易,客戶端可能忘記設置依賴,也可能設置null,另外客戶端在類聲明週期內改變依賴是也會致使不一致或不指望的行爲,這些狀況都須要本身處理。 |
private CurrencyProfileService currencyProfileService;public CurrencyProfileService CurrencyProfileService{ get { if (this.currencyProfileService == null) { this.CurrencyProfileService = new DefaultCurrencyProfileService(this.HttpContext);//默認值的延遲初始化 } return this.currencyProfileService; } set { if (value == null) { throw new ArgumentNullException("value"); } if (this.currencyProfileService != null) { throw new InvalidOperationException();//只容許依賴定義一次 } this.currencyProfileService = value; }}
每個方法的依賴都不相同時。
如何工做:依賴作爲一個方法參數
Tip1 : 首先應該確保傳入的依賴非空。
Tip2 : 若是方法並不使用傳入的依賴,最好將參數刪去,若是是實現的接口方法,那麼應該將參數驗證去掉。
什麼時候使用:每一個方法的依賴都不相同時
Tip : 方法注入和使用抽象工廠模式很類似,抽象工廠的抽象輸入能夠看做是方法注入。
優缺點:
優勢 | 缺點 |
---|---|
容許方法調用提供指定的上下文環境 | 適用性不廣 |
爲每個模塊提供依賴,而不用關注每個API。
public string GetMessage(){ return SomeContext.Current.SomeValue;}
也就是說,上面的Current必須是靜態的、抽象的、可寫的。SomeContext可能的這麼實現:
public abstract class SomeContext{ public static SomeContext Current { get { var ctx = Thread.GetData(Thread.GetNamedDataSlot("SomeContext")) as SomeContext;//一、從TLS(線程本地存儲)得到上下文 if (ctx == null) { ctx = SomeContext.Default; Thread.SetData(Thread.GetNamedDataSlot("SomeContext"), ctx); } return ctx; } set { Thread.SetData(Thread.GetNamedDataSlot("SomeContext"), value);//二、在TLS中保存上下文 } }public static SomeContext Default = new DefaultContext();public abstract string SomeValue { get; }//三、上下文承載的數據}
注意:上面的例子爲了簡單沒有考慮線程安全。自行實現時必定要考慮。
Tip1:該模式與線程和調用上下文並無關係,不少時候,使它在整個應用程序域中Static便可。
public string GetSomething(SomeService service, TimeProvider timeProvider){ return service.GetStuff("Foo", timeProvider);}
其實上面的GetStuff()
方法根本用不到timeProvider
,額外的參數污染了API。
public string GetStuff(string s, TimeProvider timeProvider){ return this.Stuff(s);}
使用條件:
優缺點
優點 | 劣勢 |
---|---|
不會污染API | 含蓄(容易引入潛在的Bug),很難被正確地實現,沒法經過接口定義來知道類的依賴關係,也不容易發現類的擴展點 |
老是能夠得到依賴 | 在一些運行時環境中不能很好的工做(好比須要切換線程上下文時) |
public abstract class TimeProvider{ private static TimeProvider current; static TimeProvider() { TimeProvider.current = new DefaultTimeProvider();//一、默認實現 } public static TimeProvider Current { get { return TimeProvider.current; } set { if (value == null)//二、確保非Null { throw new ArgumentNullException("value"); } TimeProvider.current = value; } } public abstract DateTime UtcNow { get; }//三、獲取數據 public static void ResetToDefault() { TimeProvider.current = new DefaultTimeProvider(); }}
默認實現:
public class DefaultTimeProvider : TimeProvider{ public override DateTime UtcNow { get { return DateTime.UtcNow; } }}
與控制反轉相反,描述一個類維護了它全部的依賴。最多見的反模式,使用了太多的new關鍵字,以至咱們須要控制太多實例的聲明週期。模塊都牢牢耦合在一塊兒。
private readonly ProductRepository repository;public ProductService(){ string connectionString = ConfigurationManager.ConnectionStrings["CommerceObjectContext"].ConnectionString; this.repository = new SqlProductRepository(connectionString);//直接建立了實例,緊密的耦合關係。}
反例2:工廠
最多見的想要解決new實例問題的嘗試,主要是選擇一些工廠模式。它們存在哪些問題呢?
重構
包括BCL在內,不少.NET代碼都包含重載的構造器。這個重載帶來了一些負面的影響——默認的構造器實現可能並非返回本地依賴而是一個外部依賴。當你徹底擁抱依賴注入時,這些重載都變爲是多餘的。
private readonly ProductRepository repository;public ProductService() : this(ProductService.CreateDefaultRepository()) {}//默認構造函數public ProductService(ProductRepository repository)//構造器注入{ if (repository == null) { throw new ArgumentNullException("repository"); } this.repository = repository;}private static ProductRepository CreateDefaultRepository(){ string connectionString = ConfigurationManager .ConnectionStrings["CommerceObjectContext"].ConnectionString; return new SqlProductRepository(connectionString);}
分析
這種反模式常常可見,不少開發者沒有徹底理解DI,爲了類的可測試性選擇了這種反模式。這種模式帶來了一些糟糕的影響。最重要的就是外部依賴的引入使模塊重用變得困難,同時並行開發也牢牢的依賴在一塊兒。
重構
最多見的限制是要求全部的依賴都必須有特定簽名的構造器,用來從配置文件來實現延遲綁定。
string connectionString = ConfigurationManager. ConnectionStrings["CommerceObjectContext"].ConnectionString;string productRepositoryTypeName = ConfigurationManager.AppSettings["ProductRepositoryType"];var productRepositoryType = Type.GetType(productRepositoryTypeName, true);var repository = (ProductRepository)Activator. CreateInstance(productRepositoryType, connectionString);//使用反射建立實例
這個例子中,從配置文件中讀取了鏈接字符串等一系列信息,最後反射時使用了這些信息,實際上隱式地約束了被依賴項。
約束對靈活性的影響是很是大的,好比咱們可能須要將一個單例注入不一樣的模塊。
許多開發者將靜態工廠上升到另外一個級別——服務定位器——直接控制依賴。它是模式仍是反模式是見仁見智的。DI容器和服務查找器很像,它們之間的區別是微妙的,關鍵不在於它是如何實現的,而在於你如何使用它。本質上講,若是用來在代碼基上處理完整的依賴圖,那麼它是合適的,若是在任什麼時候候獲取小顆粒的服務,那麼它是反模式。
public static class Locator{ private readonly static Dictionary<Type, object> services = new Dictionary<Type, object>(); public static T GetService<T>()//獲取服務 { return (T)Locator.services[typeof(T)]; } public static void Register<T>(T service)//註冊服務 { Locator.services[typeof(T)] = service; } public static void Reset()//清空服務 { Locator.services.Clear(); }}
這個反模式看起來很不錯,不過它是一個危險的模式。它惟一重要的問題是影響了它的消費者類的可重用性(它包含了冗餘的依賴,它不是自描述的)。想象一下兩個模塊都實現了服務查找器,或者一個使用DI,另外一個使用服務查找器。其實有更好的選擇——好比構造器注入。
readonly
字段來保存依賴;注意: 服務查找器和環境上下文模式很像,區別在於本地默認值的可用性上。後者可以保證老是返回一個合適的被請求的服務,一般只有一個。而前者是不能保證的,本質上它使用了弱類型的容器。
問題:如何處理運行時的值的依賴
使用構造器注入時,實際上要求咱們在設計時明確實際的依賴,可是有些狀況下是不能知足的,好比地圖網站,運行時依賴哪一個路徑算法,最短路徑,最少時間,仍是最少換乘。實際的依賴在運行時纔可以肯定。
解決:抽象工廠
抽象工廠模式解決的問題就是咱們能夠請求抽象的實例,它爲抽象類型和具體運行時實例直接提供了一個橋接。
示例:路徑算法
public enum RouteType//路徑類型{ Shortest = 0, Fastest, Scenic}
public interface IRouteAlgorithmFactory//工廠{ IRouteAlgorithm CreateAlgorithm(RouteType routeType);}
public IRoute GetRoute(RouteSpecification spec, RouteType routeType){ IRouteAlgorithm algorithm = this.factory.CreateAlgorithm(routeType);//映射運行時的值 return algorithm.CalculateRoute(spec);//使用映射的算法}
問題:請求外部資源
典型的好比數據庫鏈接、Web服務、資源釋放等。對於ADO.NET來講,這些都已是常識,不過對於WCF客戶端來講,若是不盡快地關閉資源,服務端的壓力會很大。
解決方案:將鏈接管理隱藏在抽象後面
一方面,依賴不能運行在內存泄漏的應用中,所以咱們必須儘快關閉鏈接。另外一方面,依賴也不能處理進程外的通訊,所以構造一個包含Close方法的抽象是有漏洞的。即便繼承IDisposable
接口,也不過是另外一種Close方法,並不能解決底層的問題。
幸運地是LINQ to SQL和LINQ to Entities爲咱們提供了思路——咱們經過context
(包含鏈接的上下文)訪問數據。
消費者類調用IResource
接口定義的方法,鏈接管理由IResource的實例進行管理。
毫無疑問,上面的方案抽象粒度比較粗,靈活性不足,有時咱們須要對依賴的生命週期進行更明確地控制,以防內存泄漏。最多見的方案就是IDisposable模式——咱們建立鏈接、使用鏈接、釋放鏈接。
固然咱們可使用實現了IDisposable模式的抽象工廠,只是消費者類必須記得釋放資源。
其實最佳實踐是使用C#的using
關鍵字。
問題:不可避免的循環依賴
只有程序存在循環的依賴關係,咱們是不可能知足全部的依賴的,所以程序也不可能運行。大多數狀況下,應該是你程序設計的問題,某些特定的實現帶來了循環依賴。若是這種實現不是必須的,你最好對程序從新設計。
典型的狀況是分層應用中循環依賴。
解決方案:
解決的第一步就是打破循環:大多數分層應用中的循環依賴是結構性錯誤,先仔細考慮下分層是否合理。思考一下循環依賴爲何發生,有時能夠改變設計,還可使用事件、觀察者模式,實在不行最後的方法是將構造器依賴注入改成屬性注入。
將B的DI方式由構造器注入改成屬性注入:
var b = new B();var a = new A(b);b.C = new C(new D(a));//屬性注入
若是你不想或者不能修改B的構造方式,還能夠引入一個虛擬的協議:
var lb = new LazyB();//和B實現同樣的接口var a = new A(lb);lb.B = new B(new C(new D(a)));
private void EnsureInitialized(){ if (this.initialized) { return; } var vm = this.vmFactory.Create(this);//建立ViewModel this.WpfWindow.DataContext = vm;//屬性注入 this.DeclareKeyBindings(vm); this.initialized = true;}
能夠在應用程序根部裝配:
IMainWindowViewModelFactory vmFactory = new MainWindowViewModelFactory(agent);Window mainWindow = new MainWindow();IWindow w = new MainWindowAdapter(mainWindow, vmFactory);
public MyClass(IUnitOfWorkFactory uowFactory, CurrencyProvider currencyProvider, IFooPolicy fooPolicy, IBarService barService, ICoffeeMaker coffeeMaker, IKitchenSink kitchenSink)
不要把過錯歸咎於構造器注入,問題在於違反了單一職責原則。
解決方案:外觀模式
外觀隱藏內部的依賴,只提供能夠消費的服務。若是系統很是大,能夠循環使用該方法
示例:一個訂單服務
引入兩個外觀接口: