天天看點

依賴倒置原則(DIP)介紹1.概念2.抽象方法3.依賴倒置方法

介紹

在本文中,我們将讨論SOLID原則的其中一個支柱,即依賴反轉原則。我們将讨論其背後的工作原理,以及如何将其應用于工作示例。

1.概念

什麼是DIP?

原則指出:

  1. 進階子產品不應依賴于低級子產品。兩者都應依賴抽象。
  2. 抽象不應依賴細節。細節應依賴于抽象。

例如,下面的代碼不符合上述原則:

public class HighLevelModule
{
  private readonly LowLevelModule _lowLowelModule;
 
  public HighLevelModule()
  {
    _lowLevelModule = new LowLevelModule();   
  }

  public void Call()
  {
    _lowLevelModule.Initiate();
    _lowLevelModule.Send();
  }
}

public class LowLevelModule
{
  public void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}
           

在上面的代碼中,HighLevelModule直接依賴于LowLevelModule并且不遵循DIP的第一點。為什麼這麼重要?兩者之間的直接且緊密耦合的關系使得在HighLevelModule上獨立于LowLevelModule建立單元測試變得更加困難。你不得不在同一時間測試HighLevelModule和LowLevelModule,因為它們是緊密耦合。

請注意,仍然可以在HighLevelModule上使用執行.NET CLR偵聽的測試架構(例如TypeMock Isolator)來隔離進行單元測試。使用此架構,可以更改測試LowLevelModule行為。但是,出于兩個原因,我不推薦這種做法。首先,在測試中使用CLR攔截違反了代碼的現實: HighLevelModule對LowLevelModule的依賴。在最壞的情況下,測試會産生假陽性結果。其次,這種做法可能會阻止我們學習編寫幹淨且可測試的代碼的技能。

我們如何應用DIP?

DIP的第一點建議我們對代碼同時應用兩件事:

  • 抽象化
  • 依賴倒置或控制反轉

首先,LowLevelModule需要被抽象,而HighLevelModule将依賴于抽象。下一節将讨論不同的抽象方法。對于下面的示例,我将使用interface進行抽象。一個IOperation接口用于抽象LowLevelModule。

public interface IOperation
{
  void Initiate();
  void Send();
}

public class LowLevelModule: IOperation
{
  public void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}
           

其次,由于HighLevelModule将僅依賴IOperation抽象,是以我們不能再在HighLevelModule類内部使用new LowLevelModule()。LowLevelModule需要從調用者上下文中注入到HighLevelModule類中。依賴項LowLevelModule需要反轉。這就是術語“依賴倒置”和“控制反轉”的來源。

需要從HighLevelModule外部傳遞LowLevelModule抽象或行為的實作,并将其從類的内部移至外部的過程稱為反轉。我将在第3節中讨論依賴倒置的不同方法。在下面的示例中,将使用通過構造函數的依賴注入。

public class HighLevelModule
{
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    _operation = operation;
  }

   public void Call()
  {
    _operation.Initiate();
    _operation.Send();
  }
}
           

我們已經将 HighLevelModule和LowLevelModule彼此分離,現在兩者都依賴于抽象IOperation。Send方法的行為可以從類之外,通過使用任何的IOperation選擇實作來控制,例如LowLevelModule

但是,尚未完成。該代碼仍然不符合DIP的第二點。抽象不應依賴于細節或實作。實際上,IOperation内的Initiate方法是的LowLevelModule實作細節,用于在執行Send操作之前準備好LowLevelModule。

我要做的是從 IOperation抽象中删除它,并将其視為LowLevelModule實作細節的一部分。我可以在LowLevelModule構造函數中包含該Initiate操作。這使操作成為一種private方法,進而限制了對類的通路。

public interface IOperation
{
  void Send();
}

public class LowLevelModule: IOperation
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

public class HighLevelModule
{
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    _operation = operation;
  }

  public void Call()
  {
    _operation.Send();
  }
}
           

2.抽象方法

實作DIP的第一個活動是将抽象應用于代碼的各個部分。在C#世界中,有幾種方法可以做到這一點:

  1. 使用接口
  2. 使用抽象類
  3. 使用委托

首先,interface僅用于提供抽象,而abstract class也可以用于提供一些共享的實作細節。最後,委托為一個特定的函數或方法提供了抽象。

附帶說明一下,将方法标記為虛方法是一種常見的做法,是以在為調用類編寫單元測試時可以模拟該方法。但是,這與應用抽象不同。将方法标記為virtual隻會使其可重寫,是以可以模拟該方法,這對于測試目的很有用。

我的偏好是将interface用于抽象目的。僅當兩個或多個類之間共享實作細節時才使用abstract類。即便如此,我也将確定abstract類實作了實際抽象的interface。在第1節中,我已經給出了使用interfaces進行抽象應用的示例。在本節中,我将使用abstract類和委托給出其他示例。

使用抽象類

使用在第1節的例子中,我隻需要更改接口IOperation為abstract類,OperationBase。

public abstract class OperationBase
{
  public abstract void Send();
}

public class LowLevelModule: OperationBase
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

public class HighLevelModule
{
  private readonly OperationBase _operation;

  public HighLevelModule(OperationBase operation)
  {
    _operation = operation;
  }

  public void Call()
  {
    _operation.Send();
  }
}
           

上面的代碼等效于使用interface。通常,隻有在共享實作細節的情況下,我才使用abstract類。例如,如果HighLevelModule可以使用LowLevelModule或AnotherLowLevelModule,并且兩個類都具有共享的實作細節,那麼我将使用一個abstract類作為兩者的基類。基類将實作IOperation,這是實際的抽象。

public interface IOperation
{
  void Send();
}

public abstract class OperationBase: IOperation
{
  public OperationBase()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending, also shared implementation in this example
  }

  public abstract void Send();
}

public class LowLevelModule: OperationBase
{
  public void Send()
  {
    //perform sending operation
  }
}

public class AnotherLowLevelModule: OperationBase
{
  public void Send()
  {
    //perform another sending operation
  }
}

public class HighLevelModule
{
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    _operation = operation;
  }

  public void Call()
  {
    _operation.Send();
  }
}
           

使用委托

可以使用委托來抽象單個方法或函數。通用委托Func<T>或Action可用于此目的。

public class Caller
{ 
  public void CallerMethod()
  {
    var module = new HighLevelModule(Send);
    ...
  }

  public void Send()
  {
    //this is the method injected into HighLevelModule
  }
}

public class HighLevelModule
{
  private readonly Action _sendOperation;

  public HighLevelModule(Action sendOperation)
  {
    _sendOperation = sendOperation;
  }

  public void Call()
  {
    _sendOperation();
  }
}
           

或者,您可以建立自己的委托并為其賦予一個有意義的名稱。

public delegate void SendOperation();

public class Caller
{ 
  public void CallerMethod()
  {
    var module = new HighLevelModule(Send);
    ...
  }

  public void Send()
  {
    //this is the method injected into HighLevelModule
  }
}

public class HighLevelModule
{
  private readonly SendOperation _sendOperation;

  public HighLevelModule(SendOperation sendOperation)
  {
    _sendOperation = sendOperation;
  }

  public void Call()
  {
    _sendOperation();
  }
}
           

使用泛型委托的好處是我們不需要為依賴項建立或實作一種類型,例如接口和類。我們可以從調用者上下文或其他任何地方使用任何方法或函數。

3.依賴倒置方法

在第一節中,我将構造函數依賴項注入用作依賴倒置方法。在本節中,我将讨論依賴倒置方法中的各種方法。

這裡是依賴倒置方法的清單:

  1. 使用依賴注入
  2. 使用全局狀态
  3. 使用間接

下面,我将解釋每種方法。

1.使用依賴注入

使用依賴注入(DI)是将依賴項通過其公共成員直接注入到類中的。可以将依賴項注入到類的構造函數(構造函數注入)、set屬性(Setter注入)、方法(方法注入)、事件,索引屬性、字段以及基本上是public類的任何成員中。我一般不建議使用字段,因為在面向對象的程式設計中,不建議将字段公開是一個好習慣,因為使用屬性可以實作相同的目的。使用索引屬性進行依賴項注入也是一種罕見的情況,是以我将不做進一步解釋。

構造函數注入

我主要使用構造函數注入。使用構造函數注入還可以利用IoC容器中的某些功能,例如自動裝配或類型發現。稍後我将在第5節中讨論IoC容器。以下是構造注入的示例:

public class HighLevelModule
{
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    _operation = operation;
  }

  public void Call()
  {
    _operation.Send();
  }
}
           

Setter 注入

Setter和Method注入用于在構造對象之後注入依賴項。與IoC容器一起使用時,這可以看作是不利條件(将在第5節中進行讨論)。但是,如果您不使用IoC容器,它們将實作與構造函數注入相同的功能。Setter或Method注入的另一個好處是允許您更改對運作時的依賴關系,它們可以用于構造函數注入的補充。下面是一個Setter注入示例,它允許您一次注入一個依賴項:

public class HighLevelModule
{
  public IOperation Operation { get; set; }

  public void Call()
  {
    Operation.Send();
  }
}
           

方法注入

使用方法注入,您可以同時設定多個依賴項。下面是方法注入的示例:

public class HighLevelModule
{
  private readonly IOperation _operationOne;
  private readonly IOperation _operationTwo;

  public void SetOperations(IOperation operationOne, IOperation operationTwo)
  {
    _operationOne = operationOne;
    _operationTwo = operationTwo;
  }

  public void Call()
  {
    _operationOne.Send();
    _operationTwo.Send();
  }
}
           

使用方法注入時,作為參數傳遞的依賴項将保留在類中,例如作為字段或屬性,以備後用。在方法中傳遞某些類或接口并僅在方法中使用時,這不算作方法注入。

使用事件

僅在委托類型注入中使用事件才受限制,并且僅在需要訂閱和通知模型的情況下才适用,并且委托不得傳回任何值,或僅傳回void。調用者将向實作該事件的類訂閱一個委托,并且可以有多個訂閱者。事件注入可以在對象構造之後執行。通過構造函數注入事件并不常見。以下是事件注入的示例。

public class Caller
{ 
  public void CallerMethod()
  {
    var module = new HighLevelModule();
    module.SendEvent += Send ;

    ...
  }

  public void Send()
  {
    //this is the method injected into HighLevelModule
  }
}

public class HighLevelModule
{
  public event Action SendEvent = delegate {};

  public void Call()
  {
    SendEvent();
  }
}
           

通常,我的口頭禅始終是使用構造函數注入,如果沒有什麼可迫使您使用Setter或Method注入的話,這也使我們能夠在以後使用IoC容器。

2.使用全局狀态

可以從類内部的全局狀态中檢索依賴關系,而不必直接注入到類中。可以将依賴項注入全局狀态,然後從類内部進行通路。

public class Helper
  {
    public static IOperation GlobalStateOperation { get; set;}
  }

  public class HighLevelModule
  {
    public void Call()
    {
       Helper.GlobalStateOperation.Send();
    }
  }
   
  public class Caller
  {
    public void CallerMethod()
    {
      Helper.GlobalStateOperation = new LowLevelModule();

      var highLevelModule = new HighLevelModule();
      highLevelModule.Call();
    }
  }  
}
           

全局狀态可以表示為屬性、方法甚至字段。重要的一點是,基礎值具有公共setter和getter。setter和getter可以采用方法而不是屬性的形式。

如果全局狀态隻有getter(例如,單例),則依賴性不會反轉。不建議使用全局狀态來反轉依賴關系,因為它會使依賴關系變得不那麼明顯,并将它們隐藏在類中。

3.使用間接

如果使用的是Indirect,則不會直接将依賴項傳遞給類。而是傳遞一個能夠為您建立或傳遞抽象實作的對象。這也意味着您為該類建立了另一個依賴關系。您傳遞給類的對象的類型可以是:

  • 系統資料庫/容器對象
  • 工廠對象

您可以選擇是直接傳遞對象(依賴注入)還是使用全局狀态。

系統資料庫/容器對象

如果使用系統資料庫(通常稱為服務定位器模式),則可以查詢系統資料庫以傳回抽象的實作(例如接口)。但是,您将需要先從類外部注冊實作。您也可以像許多IoC容器架構一樣使用容器來包裝系統資料庫。容器通常具有其他類型的發現或自動裝配功能,是以,在注冊容器interface及其實作時,無需指定實作類的依賴項。當查詢接口時,容器将能夠通過首先解決其所有依賴關系來傳回實作類執行個體。當然,您将需要首先注冊所有依賴項。

在IoC容器架構如雨後春筍般出現的早期,該容器通常被實作為Global狀态或Singleton,而不是将其顯式傳遞給類,是以現在被視為反模式。這是使用容器類型對象的示例:

public interface IOperation
{
  void Send();
}

public class LowLevelModule: IOperation
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

public class HighLevelModule
{
  private readonly Container _container;

  public HighLevelModule(Container container)
  {
    _container = container;
  }

  public void Call()
  {
    IOperation operation = _container.Resolvel<IOperation>();
    operation.Send();
  }
}

public class Caller
{
  public void UsingContainerObject()
  {
     //registry the LowLevelModule as implementation of IOperation
     var register  = new Registry();
     registry.For<IOperation>.Use<LowLevelModule>();

     //wrap-up registry in a container
     var container = new Container(registry);
      
     //inject the container into HighLevelModule
     var highLevelModule = new HighLevelModule(container);
     highLevelModule.Call();     
  }
}
           

您甚至可以使HighLevelModule依賴于容器的抽象,但是此步驟不是必需的。

而且,在類中的任何地方使用容器或系統資料庫可能不是一個好主意,因為這使類的依賴關系變得不那麼明顯。

工廠對象

使用系統資料庫/容器和工廠對象之間的差別在于,使用系統資料庫/容器時,需要先注冊實作類才能查詢它,而使用工廠時則不需要這樣做,因為執行個體化是在工廠實作中進行了寫死。工廠對象不必将“工廠”作為其名稱的一部分。它可以隻是傳回抽象(例如,接口)的普通類。

此外,由于LowLevelModule執行個體化是在工廠實作中進行寫死的,是以HighLevelModule依賴工廠不會導緻LowLevelModule依賴關系反轉。為了反轉依賴關系,HighLevelModule需要依賴于工廠抽象,而工廠對象需要實作該抽象。這是使用工廠對象的示例:

public interface IOperation
{
  void Send();
}

public class LowLevelModule: IOperation
{
  public LowLevelModule()
  {
    Initiate();
  }

  private void Initiate()
  {
    //do initiation before sending
  }
  
  public void Send()
  {
    //perform sending operation
  }
}

public interface IModuleFactory
{
   IOperation CreateModule();
}

public class ModuleFactory: IModuleFactory
{
  public IOperation CreateModule()
  {
      //LowLevelModule is the implementation of the IOperation, 
      //and it is hardcoded in the factory. 
      return new LowLevelModule();
  }
}

public class HighLevelModule
{
  private readonly IModuleFactory _moduleFactory;

  public HighLevelModule(IModuleFactory moduleFactory)
  {
    _moduleFactory = moduleFactory;
  }

  public void Call()
  {
    IOperation operation = _moduleFactory.CreateModule();
    operation.Send();
  }
}

public class Caller
{
  public void CallerMethod()
  {
     //create the factory as the implementation of abstract factory
     IModuleFactory moduleFactory = new ModuleFactory();
      
     //inject the factory into HighLevelModule
     var highLevelModule = new HighLevelModule(moduleFactory);   
     highLevelModule.Call();  
  }
}
           

我的建議是謹慎使用間接。服務定位器模式,如今被視為反模式。但是,有時可能需要使用工廠對象為您建立依賴關系。我的理想是避免使用間接,除非證明有必要。

除了抽象(接口、抽象類或代理)的執行情況,我們通常可以還注入依賴于原始類型,諸如布爾,int,double,string或隻是一個隻包含屬性的類。

繼續閱讀