介绍

在本文中,我们将讨论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 IOperationvar register  = new Registry();registry.For<IOperation>.Use<LowLevelModule>();//wrap-up registry in a containervar container = new Container(registry);//inject the container into HighLevelModulevar 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 factoryIModuleFactory moduleFactory = new ModuleFactory();//inject the factory into HighLevelModulevar highLevelModule = new HighLevelModule(moduleFactory);   highLevelModule.Call();  }
}

我的建议是谨慎使用间接。服务定位器模式,如今被视为反模式。但是,有时可能需要使用工厂对象为您创建依赖关系。我的理想是避免使用间接,除非证明有必要。

除了抽象(接口、抽象类或代理)的执行情况,我们通常可以还注入依赖于原始类型,诸如布尔,int,double,string或只是一个只包含属性的类。

依赖倒置原则(DIP)相关推荐

  1. 7.12 其他面向对象设计原则3: 依赖倒置原则DIP

    其他面向对象设计原则3: 依赖倒置原则DIP  The Dependency Inversion Principle 7.1 依赖倒置原则DIP The Dependency Inversion P ...

  2. C#软件设计——小话设计模式原则之:依赖倒置原则DIP

    前言:很久之前就想动笔总结下关于软件设计的一些原则,或者说是设计模式的一些原则,奈何被各种bootstrap组件所吸引,一直抽不开身.群里面有朋友问博主是否改行做前端了,呵呵,其实博主是想做" ...

  3. 吐槽 依赖倒置原则/DIP

    返回目录   1.2.5抛弃依赖倒置原则(正文) ,本文专门吐槽依赖倒置原则(Dependency Inversion Principle.DIP). 1.Robert C. Martin的那篇DIP ...

  4. 设计原则(单一职责原则 开放封闭原则 里氏替换原则 依赖倒置原则 接口隔离原则 迪米特法则)

    设计原则 单一职责原则(SRP) 从三大特性角度看原则: 应用的设计模式: 开放封闭原则(OCP) 从三大特性角度看原则: 应用的设计模式: 里氏替换原则(LSP) 从三大特性角度看原则: 应用的设计 ...

  5. DIP(依赖倒置原则),IoC(控制反转),DI(依赖注入)复习总结

    原文地址:http://blog.csdn.net/qqlinke/archive/2011/04/05/6303689.aspx ---- 概念 ---- ◆1.依赖倒置原则(DIP,Depende ...

  6. 设计模式六大原则之--依赖倒置原则(DIP)

    1. 依赖倒置原则,(Dependence Inversion Principle, DIP ) 定义:High level modules should not depend upon low le ...

  7. 3.六大原则例子-- 依赖倒置原则(DIP)例子

    设计模式六大原则例子-- 依赖倒置原则(DIP)例子 之前我们对设计模式的六大原则做了简单归纳,这篇博客是对依赖倒置原则进行的举例说明. 依赖倒置原则的意义 DIP是6大原则中最难以实现的原则,它是实 ...

  8. java 依赖倒置_设计模式之三依赖倒置原则(DIP)

    依赖倒置(Dependence Inversion Principle,DIP) High level modules should not deppend oupon low level modul ...

  9. 依赖倒置原则(DIP)、控制反转(IoC)、依赖注入(DI)(C#)

    象的控制权交由配置文件控制,然后根据配置文件中的信息(程序集+类型),通过反射来获取对象,而不是直接new对象,这也是控制反转的一种体现. IoC容器会连接程序中的所有模块,模块将所需对象的控制权都交 ...

  10. 设计模式原则之依赖倒置原则

    所谓依赖倒置原则(Dependence Inversion Principle )就是要依赖于抽象,不要依赖于具体.简单的说就是对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合. ...

最新文章

  1. MySQL重置root用户密码的方法
  2. postman无法获得响应_【原创翻译】POSTMAN从入门到精通系列(二):发送第一个请求...
  3. 通过grub硬盘安装centos7
  4. android file.createnewfile ioexception
  5. 前端实习生笔试_2016春网易前端暑期实习生笔试面经(二面已挂)
  6. 【SharePoint 2010】将Sharepoint Server 2010部署到WINDOWS 7
  7. pacf和acf_如何通过Wordpress API,ACF和Express.js使Wordpress更加令人兴奋
  8. wince6.0编译命令分析
  9. 不用更改注册表就可以更改桌面所在的位置
  10. 这个机器人花盆,给你的植物长了脚脚
  11. 数值分析方阵的QR分解
  12. git 小乌龟 推送代码到gitee
  13. matlab 球体的绘制 柱面坐标系法 球面坐标系法
  14. 新手入门吉他买什么好?十年吉他老司机教你如何远离烧火棍,附上靠谱吉他品牌推荐!
  15. c语言 按键切换显示屏,51单片机lcd1602按键切屏
  16. 【OpenCV3】直线拟合--FitLine()函数详解
  17. phpstudy安装和使用
  18. wcs开发_WCS 5.2的评论—用于Webcast和Webcam开发人员的WebRTC服务器
  19. scala 自带json_在scala中格式化JSON字符串
  20. Android 无障碍服务自动点击

热门文章

  1. python读取特定单词_在文本python中搜索特定单词
  2. 平方根倒数速算法(Fast Inverse Square Root)
  3. 列表, 元组, range() 知识总结
  4. 伯明翰大学计算机科学硕士学费,2020年伯明翰大学计算机科学专业研究生申请条件及世界排名|学费介绍...
  5. tranmac不能识别_Windows 10下使用TransMac制作Yosemite安装优盘无法识别EFI分区
  6. FFMPEG录制以及推流
  7. RNGUZI疑是玩电竞竞猜的APPO(∩_∩)O哈哈~APP?QQ是什么让他输?qun是放水吗?还是身体不适91435456?
  8. fcpx如何用光流法_熟悉这些fcpx剪辑技巧 快速提高你的剪辑效率
  9. 短视频运营:如何提高自己的剪辑技巧?
  10. 浙江大学计算机专业夏令营,浙江大学计算机科学与技术学院数字化艺术与设计保研夏令营...