編碼最佳實踐——依賴注入原則

咱們在這個系列的前四篇文章中分別介紹了SOLID原則中的前四個原則,今天來介紹最後一個原則——依賴注入原則。依賴注入(DI)是一個很簡單的概念,實現起來也很簡單。可是簡單卻掩蓋不了它的重要性,若是沒有依賴注入,前面的介紹的SOLID技術原則都不可能實際應用。算法

控制反轉(IoC)

人們在談論依賴注入的時候,常常也會談到另外一個概念——控制反轉(IoC)。按照大內老A的解釋:「IoC主要體現了這樣一種設計思想:經過將一組通用流程的控制權從應用轉移到框架中以實現對流程的複用,並按照「好萊塢法則」實現應用程序的代碼與框架之間的交互「。概念比較抽象,咱們拆開解讀一下。編程

咱們要作的任何一件事情,不管大小,均可以分解爲相應的步驟。因此任何一件事情都有其固定的流程。與現實問題域同樣,解決方案域(程序實現)也是這樣。因此IoC控制能夠理解爲「對流程的控制」。以HTTP請求處理的流程爲例,在傳統面向類庫編程的時代,針對HTTP請求處理的流程緊緊控制在應用程序手中。在引入框架以後,請求處理的控制權轉移到了框架手上。類庫(Library)和框架(Framework)的不一樣之處在於,前者每每只是提供實現某種單一功能的API,然後者則針對一個目標任務對這些單一功能進行編排造成一個完整的流程,這個流程在一個引擎的驅動下自動執行。如此,全部使用此框架的程序均可以複用關於HTTP請求處理的流程。c#

在好萊塢,把簡歷遞交給演藝公司後就只有回家等待。由演藝公司對整個娛樂項目的徹底控制,演員只能被動式的接受電影公司的工做,在須要的環節中,完成本身的演出。「不要給咱們打電話,咱們會給你打電話(don‘t call us, we‘ll call you)」這是著名的好萊塢法則。設計模式

mark

IoC完美地體現了這一法則,對於ASP.NET MVC應用開發來講,咱們只須要按照約定規則(好比目錄結構和命名等)定義相應的Controller類型和View文件就能夠了,這就是所謂的「約定大於配置」。當ASP.NET MVC框架在進行處理請求的過程當中,它會根據解析生成的路由參數定義爲對應的Controller類型,並按照預約義的規則找到咱們定義的Controller,而後自動建立並執行它。若是定義在當前Action方法須要呈現一個View,框架自身會根據預約義的目錄約定找到咱們定義的View文件,並對它實施動態編譯和執行。整個流程到處體現了「框架Call應用」的好萊塢法則。微信

簡單的說,控制反轉(IoC)的過程就是一組通用流程的控制權從應用程序轉移到框架中的過程,爲的是實現流程的複用。可是有一個問題,被反轉的僅僅是一個泛化的流程,在特定場景可能會有一些特殊的流程或者流程節點,此時就須要進行流程定製。定製通常是經過框架預留的擴展點進行的,好比ASP.NET中的HttpHandler和HttpModule,ASP.NET Core中的Middleware。架構

前面提到控制反轉(IoC)是一種設計思想。因此控制反轉(IoC)並不能解決某一類具體的問題。可是基於控制反轉(IoC)思想的設計模式卻能夠,最簡單直觀的就是模板方法模式。該模式主張將一個可複用的工做流程或者由多個步驟組成的算法定義成模板方法,組成這個流程或者算法的步驟實如今相應的虛方法之中,模板方法根據按照預先編排的流程去調用這些虛方法。全部這些方法均定義在同一個類中,咱們能夠經過派生該類並重寫相應的虛方法達到對流程定製的目的。框架

public class TemplateMethod
{
    //流程編排
    public void ABCD()
    {
        A();
        B();
        C();
        D();
    }
    //步驟A
    protected virtual void A() { }
    //步驟B
    protected virtual void B() { }
    //步驟C
    protected virtual void C() { }
    //步驟D
    protected virtual void D() { }
}

依賴注入(DI)

依賴注入(DI)也是架構在控制反轉思想上的一種模式。在這裏咱們將提供的對象統稱爲「服務」、「服務對象」或者「服務實例」。在一個採用DI的應用中,在定義某個服務類型的時候,咱們直接將依賴的服務採用相應的方式注入進來。按照「面向接口編程」的原則,被注入的最好是依賴服務的接口而非實現。正確的依賴注入對於項目的絕大多數代碼都是不可見的,它們(註冊代碼)被侷限在一個很小的代碼範圍內,一般是一個獨立的程序集。函數

在應用啓動的時候,會對所需的服務進行全局註冊。服務通常都是針對接口進行註冊的,服務註冊信息的核心目的是爲了在後續消費過程當中可以根據接口建立或者提供對應的服務實例。按照「好萊塢法則」,應用只須要定義好所需的服務,服務實例的激活和調用則徹底交給框架來完成,而框架則會採用一個獨立的「容器(Container)」來提供所需的每個服務實例。咱們將這個被框架用來提供服務的容器稱爲「DI容器」,也由不少人將其稱爲「IoC容器」。全部的DI容器都符合註冊、解析、釋放模式post

依賴注入的三種注入方式

1.構造函數注入學習

public class TaskService
{
    private ITaskOneRepository taskOneRepository;
    private ITaskTwoRepository taskTwoRepository;
    public TaskService(
        ITaskOneRepository taskOneRepository,
        ITaskTwoRepository taskTwoRepository)
        {
            this.taskOneRepository = taskOneRepository;
            this.taskTwoRepository = taskTwoRepository;
        }
}

優勢:

  • 在構造方法中體現出對其餘類的依賴,一眼就能看出這個類須要其餘那些類才能工做。
  • 脫離了IOC框架,這個類仍然能夠工做(窮人的依賴注入)。
  • 一旦對象初始化成功了,這個對象的狀態確定是正確的。

缺點:

  • 構造函數會有不少參數。
  • 有些類是須要默認構造函數的,好比MVC框架的Controller類,一旦使用構造函數注入,就沒法使用默認構造函數。

2.屬性注入

public class TaskService
{
    private ITaskRepository taskRepository;
    private ISettings settings;
    public TaskService(
        ITaskRepository taskRepository,
        ISettings settings)
        {
            this.taskRepository = taskRepository;
            this.settings = settings;
        }
    public void OnLoad()
    {
        taskRepository.settings = settings;
    }
}

優勢:

  • 在對象的整個生命週期內,能夠隨時動態的改變依賴。
  • 很是靈活。

缺點:

  • 對象在建立後,被設置依賴對象以前這段時間狀態是不對的(從構造函數注入的依賴實例在類的整個生命週期內均可以使用,而從屬性注入的依賴實例還能從類生命週期的某個中間點開始起做用)。
  • 不直觀,沒法清晰地表示哪些屬性是必須的。

3.方法注入

public class TaskRepository
{
    private ISettings settings;

    public void PrePare(ISettings settings)
    {
        this.settings = settings;
    }
}

優勢:

  • 比較靈活。

缺點:

  • 新加入依賴時會破壞原有的方法簽名,若是這個方法已經被其餘不少模塊用到就很麻煩。
  • 與構造方法注入同樣,會有不少參數。

在這三種注入方式中,推薦使用構造函數注入。最重要的緣由是服務應該是獨立自治的,即便脫離了DI框架,這個服務應該仍然能夠工做。構造函數注入就符合這一要求,即便脫離了DI框架,仍然能夠手動注入依賴的服務。

依賴注入反模式 —— Service Locator

假設咱們須要定義一個服務類型C,它依賴於另外兩個服務A和B,後者對應的服務接口分別爲IA和IB。若是當前應用中具備一個DI容器(Container),那麼咱們能夠採用以下兩種方式來定義這個服務類型C。

public class C : IC
{
    public IA A { get; }
    public IB B { get; }
    public C(IA a, IB b)
    {
        A = a;
        B = b;
    }
    public void Invoke()
    {
        a.Invoke();
        b.Invoke();
    }
}

public class C : IC
{
    public Container Container { get; }
    public C(Container container)
    {
        Container = container;
    }
    public void Invoke()
    {
        Container.GetService<IA>().Invoke();
        Container.GetService<IB>().Invoke();
    }
}

從表面上看,這兩種方式並無什麼太大的區別。都解決了針對依賴服務的耦合問題,將針對服務實現依賴變成針對接口的依賴。可是,其實後一種方式並非依賴注入模式,而是服務定位器反模式。由於看起來和依賴注入模式很類似,人們常常會忽視它給代碼帶來的破壞。

咱們能夠從「DI容器」和「Service Locator」被誰使用的角度來區分這兩種設計模式的差異。DI容器的使用者是框架而不是應用程序,Service Locator的使用者是應用程序,應用程序利用它來提供服務實例。有時候,它是惟一能提供依賴注入鉤子的方式。

那麼Service Locator(服務定位器反模式)對代碼形成了哪些破壞呢?

  1. 由於容器中的服務是全局註冊的,因此DI容器是靜態的,這會致使出現靜態類或者服務中出現靜態變量和字段。
  2. 服務定位器暴露了容器存在的信息。緣由是服務定位器容許類檢索任何對象,不管是否合適。這樣違背了依賴注入的「好萊塢準則」,不要調用咱們,咱們會調用你。
  3. 服務定位器會直接委託Container實例來解析實例對象,這樣會形成服務沒有依賴的假象。可是服務確定是有依賴的,否則爲何要從服務定位器獲取它們呢。

雖然咱們對服務定位器反模式提出了這麼多批判,可是它仍是很是常見。由於有時候根本沒有從構造函數注入的任何機會,惟一的選擇就是服務定位器。畢竟它確定比不注入依賴要好,也比手動構造注入依賴要好。

總結

依賴注入(DI)是架構在控制反轉(IoC)思想上的一種模式,全部的DI容器都符合註冊、解析、釋放模式。注入代碼一般在一個獨立的程序集,注入的最好是依賴服務的接口而非實現,服務實例的激活和調用則徹底交給框架來完成。在依賴注入的三種注入方式中,推薦使用構造函數注入。另外在沒有從構造函數注入的機會時,能夠考慮選擇服務定位器反模式。選擇模式的原則是:依賴注入模式優於服務定位器反模式,優於手動構造注入依賴,優於不注入依賴

參考

依賴注入1: 控制反轉

依賴注入2: 基於IoC的設計模式

依賴注入3: 依賴注入模式

《C#敏捷開發實踐》

做者:CoderFocus

微信公衆號:

聲明:本文爲博主學習感悟總結,水平有限,若是不當,歡迎指正。若是您認爲還不錯,不妨點擊一下下方的推薦按鈕,謝謝支持。轉載與引用請註明做者及出處。

相關文章
相關標籤/搜索