依赖倒置原则(DIP)

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拦截违反了代码的现实:对HighLevelModuleon的依赖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抽象,因此我们不能再 new LowLevelModule()在HighLevelModule类内部使用。LowLevelModule 需要HighLevelModule从调用者上下文中注入到类中。依赖项LowLevelModule需要反转。这就是术语“依赖倒置”和“控制倒置”的来源。

LowLevelModule需要从外部传递抽象或行为的实现,HighLevelModule将其从类的内部移至外部的过程称为反转。我将在第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的第二点。抽象不应取决于细节或实现。实际上,该Initiate方法IOperation是的实现细节LowLevelModule,用于LowLevelModule在执行Send操作之前准备好。

我要做的是从抽象中删除它 IOperation,并将其视为LowLevelModule实现细节的一部分。我可以Initiate在LowLevelModule构造函数中包含该操作。这使操作成为一种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#世界中,有几种方法可以做到这一点:

使用介面
使用抽象类
使用委托
首先,aninterface仅用于提供抽象,而anabstract class 也可以用于提供一些共享的实现细节。最后,委托为一个特定的函数或方法提供了抽象。

附带说明一下,将方法标记为虚拟是一种常见的做法,因此在为调用类编写单元测试时可以模拟该方法。但是,这与应用抽象不同。将方法标记为虚拟只会使其可重写,因此可以模拟该方法,这对于测试目的很有用。

我的偏好是将aninterface用于抽象目的。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或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)是将依赖项通过其公共成员直接注入到类中的地方。可以将依赖项注入到类的构造函数(构造函数注入),集合属性(Setter注入),方法(方法注入),事件,索引属性,字段以及基本上是类的任何成员中public。我通常不建议使用字段,因为在面向对象的编程中,不建议将字段公开为好习惯,因为使用属性可以实现相同的目的。使用索引属性进行依赖项注入也是一种罕见的情况,因此我将不做进一步解释。

构造函数注入
我主要使用构造函数注入。使用构造函数注入还可以利用IoC容器中的某些功能,例如自动装配或类型发现。稍后我将在第5节中讨论IoC容器。以下是构造注入的示例:

public class HighLevelModule
{
    
    
  private readonly IOperation _operation;

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

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

二传手注射
Setter和Method Injection用于在构造对象之后注入依赖项。与IoC容器一起使用时,这可以看作是不利条件(将在第5节中进行讨论)。但是,如果您不使用IoC容器,它们将实现与构造函数注入相同的功能。Setter或Method Injection的另一个好处是允许您更改对运行时的依赖关系,它们可以用于构造函数注入的补充。下面是一个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 Injection的话,这也使我们能够在以后使用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可以采用方法而不是属性的形式。

如果全局状态只有吸气剂(例如,单例),则依赖性不会反转。不建议使用全局状态来反转依赖关系,因为它会使依赖关系变得不那么明显,并将它们隐藏在类中。

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或只是一个类只包含属性。

猜你喜欢

转载自blog.csdn.net/u014249305/article/details/110355504