依賴注入 – ASP.NET MVC 4 系列

       從 ASP.NET MVC 3.0 開始就引入了一個新概念:依賴解析器(dependence resolver)極大的加強了應用程序參與依賴注入的能力,更好的在 MVC 使用的服務和建立的一些類(控制器和視圖頁面)之間創建依賴關係。爲更好的理解依賴解析器的工做原理,下面首先定義一些它所用到的通用軟件模式。數據庫

軟件設計模式

       軟件設計模式主要用來規範問題及其解決方案的描述,簡化開發人員對常見問題及其對應解決方案的標識與交流。設計模式並非新奇的發明,而是爲行業中常見的實踐給出一個正式的名稱和定義。設計模式

設計模式 - 控制反轉模式

       幾乎每一個人都見過或編寫過相似下面的代碼:app

public class EmailService
{
    public void SendMessage()
    {
        //...
    }
}
public class NotificationSystem
{
    private EmailService svc;
 
    public NotificationSystem()
    {
        svc = new EmailService();
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }    
}

       上面的代碼中,NotificationSystem 類依賴於 EmailService 類。當一個組件依賴於其餘組件時,咱們稱其爲耦合(coupling)。本例中,通知系統(NotificationSystem)在其構造方法內部直接建立 e-mail 服務的一個實例,換言之,通知系統明確地知道建立和使用了哪一種類型的服務。這種耦合表示了代碼的內部連接性。一個類知道與其交互的類的大量信息(正如上面的示例),咱們稱其爲高耦合函數

       在軟件設計過程當中,高耦合一般認爲是軟件設計的責任,當一個類精確的知道另外一個類的設計和實現時,就會增長軟件修改的負擔,由於修改一個類頗有可能破壞依賴於它的另外一個類。上面的代碼還存在一個問題,當感興趣的事件發生時,通知系統如何發送其餘類型的信息?例如系統管理員可能想獲得文本信息,或者想把每一個通知都記錄到數據庫中,爲了實現這些功能,咱們必須從新實現 NotificationSystem 類。this

       爲下降組件之間的耦合程度,通常採起兩個獨立但相關的步驟:spa

1. 在兩塊代碼之間引入抽象層設計

       在 .NET 平臺中,一般使用接口(或抽象類)來表明兩個類之間的抽象層。咱們能夠引入一個接口,並確保編寫的代碼只調用接口中的方法和屬性,這樣一來,NotificationSystem 類中的私有副本(svc)就變成了一個接口的實例,而再也不是具體類型:rest

public interface IMessagingService
{
    void SendMessage();
}
 
public class EmailService : IMessagingService
{
    public void SendMessage()
    {
        //...
    }
}
 
public class NotificationSystem
{
    private IMessagingService svc;
 
    public NotificationSystem()
    {
        svc = new EmailService();
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }
}

2. 把選擇抽象實現的責任移到消費者類的外部code

       須要把 EmailService 類的建立(svc = new EmailService( ))移到 NotificationSystem 類的外面。把依賴的建立移到使用這些依賴的類的外部,這就是控制反轉模式!之因此這樣命名,是由於反轉的是依賴的建立,也正由於如此,才消除了消費者類對依賴建立的控制。控制反轉(IoC)模式是抽象的,它只是表述應該從消費者類中移出依賴建立,而沒有表述如何實現。下面咱們將探討用控制反轉模式實現責任轉移的兩種經常使用方法服務定位器依賴注入(重點)對象

設計模式 - 服務定位器

       服務定位器模式是控制反轉模式的一種實現方式,它經過一個稱爲服務定位器的外部組件來爲須要依賴的組件提供依賴。服務定位器有時是一個具體的接口,爲特定服務提供強類型的請求;有時又多是一個泛型類型,能夠提供任意類型的請求服務。

1. 強類型服務定位器

       對應示例程序的強類型服務定位器可能有以下接口:

public interface IServiceLocator
{
    IMessagingService GetMessagingService();
}
       在本例中,當須要一個實現了 IMessagingService 接口的對象時,咱們知道應該調用 GetMessagingService 方法,它會返回一個 IMessagingService 接口對象,所以並不須要轉換結果的類型。這裏把服務定位器做爲一個接口,而不是一個具體的類型。咱們的目標是下降組件之間的耦合程度,其中包括消費者代碼和服務定位器之間的耦合。若是消費者代碼實現了 IServiceLocator 接口,就能夠在運行時環境中選擇合適的實現方式。

       要用強類型的服務定位器從新編寫 NotificationSystem 類,代碼以下:

public class NotificationSystem
{
    private IMessagingService svc;
 
    public NotificationSystem(IServiceLocator locator)
    {
        svc = locator.GetMessagingService();
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }
}

       上面的代碼假設建立 NotificationSystem 實例的每一個人都會訪問服務定位器。這樣作帶來的便利是,若是應用程序經過服務定位器建立 NotificationSystem 實例,那麼定位器將自身傳遞到 NotificationSystem 類的構造函數中。

      

2. 弱類型服務定位器

       若是在某個應用中,強類型服務定位器的負面影響超過了它所帶來的正面效應,能夠考慮改用弱類型服務定位器:

public interface IServiceLocator
{
    object GetService(Type serviceType);
}

       這種變體更加靈活,由於它採用 Type 類型的參數,並返回一個 Object 類型的對象。固然,須要把調用 GetService 方法返回的結果轉換爲正確類型的對象。

       使用弱類型服務定位器的 NotificationSystem 類的代碼以下:

public class NotificationSystem
{
    private IMessagingService svc;
 
    public NotificationSystem(IServiceLocator locator)
    {
        svc = (IMessagingService)locator.GetService(typeof(IMessagingService));
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }
}

       這樣的代碼並不簡潔,這主要是由於調用 GetService 後須要轉換類型。自從 .NET 2.0 引入泛型以來,就能夠包含 GetService 方法的泛型版本:

public interface IServiceLocator
{
    object GetService(Type serviceType);
    TService GetService<TService>();
}

       按照泛型方法的約定,它將返回一個已經轉換爲正確類型的對象,注意,返回的類型是 TService 而不是 Object,這使得 NotificationSystem 類的代碼變得簡潔一些:

public class NotificationSystem
{
    private IMessagingService svc;
 
    public NotificationSystem(IServiceLocator locator)
    {
        svc = locator.GetService<IMessagingService>();
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }
}

       Object 版本的 GetService 仍然存在的意義,是由於並不是每個調用 API 的消費者在編譯時都精確地知道它們將要調用的類型。如今這樣作的負面影響是,它強制 IServiceLocator 接口必須實現兩個幾乎相同的方法!這些無謂的努力在 .NET 3.5 中被移除,由於一個新特性:擴展方法

       把擴展方法做爲靜態類的靜態方法來編寫,在它的第一個參數中利用特殊的 this 關鍵字來指定擴展方法要附加到的類型。

       把 GetService 泛型方法分割成爲擴展方法後,代碼以下:

public interface IServiceLocator
{
    object GetService(Type serviceType);       
}
 
public static class ServiceLocatorExtensions
{
    public static TService GetService<TService>(this IServiceLocator locator)
    {
        return (TService)locator.GetService(typeof(TService));
    }
}

3. 服務定位器的利弊

       服務定位器的用法比較簡單:首先,咱們從某個地方獲得服務定位器,而後利用定位器查找依賴。可能在一個已知的(全局)位置找到服務定位器,或者經過咱們的建立者得到服務定位器。儘管依賴關係有時會發生改變,但簽名不會變,由於查找依賴惟一須要的就是定位器。持久簽名帶來好處的同時,也帶來了弊端。它致使了組件需求的不透明性:使用組件的開發人員經過查看構造函數的簽名不能知道服務要求的是什麼。

       需求的不透明性促使咱們選擇下一個反轉控制模式:依賴注入

設計模式 - 依賴注入

       依賴注入(Dependency Injection,DI)是另外一種控制反轉模式的形式,它沒有像服務定位器同樣的中間對象。相反,組件以一種容許依賴的方式來編寫,一般由構造函數參數或屬性設置器來顯式表示。

       若是採用構造函數注入,NotificationSystem 類的代碼將以下所示:

public class NotificationSystem
{
    private IMessagingService svc;
 
    public NotificationSystem(IMessagingService service)
    {
        this.svc = service;
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }
}

       這段代碼有 3 個顯著優勢:

  1. 極大的簡化了構造函數的實現,組件老是指望建立它的類可以傳遞須要的依賴,而它只需存儲 IMessagingService 接口的實例以便以後使用!(IMessagingService 的實現能夠有各類版本,好比發送 email、發送文本消息,這就是消費者類調用接口而不是隻調用 Email 類的好處)
  2. 這段代碼減小了 NotificationSystem 類須要知道的信息量。在之前,NotificationSystem 既須要知道服務定位器,也須要知道它本身的依賴項,而如今只須要知道它本身的依賴項就好了
  3. 需求的透明性。任何想建立 NotificationSystem 類實例的代碼都能查看構造函數,並精確知道哪些內容是使用 NotificationSystem 類必需的。

      

       屬性注入(property injection)是一種不太常見的依賴注入方式,該方式經過設置對象上的公有屬性而不是經過構造函數傳遞參數來注入依賴

       若是採用屬性注入,NotificationSystem 類的代碼將以下所示:

public class NotificationSystem
{
    public IMessagingService MessagingService { get; set; }
 
    public void InterestingEventHappened()
    {
        MessagingService.SendMessage();
    }
}

       如今,NotificationSystem 類但願任何消費者類都經過屬性來提供依賴。如今的 InterestingEventHappened 方法可能會產生一個空引用對象的異常,若是它被調用時並無提供服務依賴(沒有對屬性來賦值)。應作如下完善以確保使用服務以前已提供了服務依賴:

public void InterestingEventHappened()
{
    if (this.MessagingService == null)
    {
        throw new InvalidOperationException("Please set MessagingService before calling InterestingEventHappened().");
    }
    MessagingService.SendMessage();
}

       屬性注入下降了透明性(沒法經過構造函數知曉依賴),也比構造函數注入更容易產生錯誤,那麼開發人員爲什麼還要這樣使用呢?究其緣由,主要有 2 點:

  1. 若是依賴在某種意義上是真正可選的,即在消費者類不提供依賴時,也有相應的處理(好比說,在 MessagingService 的 get 中有一個默認的初始化動做)。
  2. 類的實例可能須要在咱們尚未控制調用的構造函數的狀況下被建立,這個緣由並不太明顯。

       一般狀況下,開發人員更傾向於使用構造函數注入!

相關文章
相關標籤/搜索