介紹
在本文中,我們将讨論SOLID原則的其中一個支柱,即依賴反轉原則。我們将讨論其背後的工作原理,以及如何将其應用于工作示例。
1.概念
什麼是DIP?
原則指出:
- 進階子產品不應依賴于低級子產品。兩者都應依賴抽象。
- 抽象不應依賴細節。細節應依賴于抽象。
例如,下面的代碼不符合上述原則:
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#世界中,有幾種方法可以做到這一點:
- 使用接口
- 使用抽象類
- 使用委托
首先,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.使用依賴注入
使用依賴注入(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或隻是一個隻包含屬性的類。