什么是依赖倒置原则,为什么它很重要?
C#中的敏捷软件开发,原理,模式和实践以及敏捷原则,模式和实践是完全理解依赖性倒置原则背后的原始目标和动机的最佳资源."依赖倒置原则"这一文章也是一个很好的资源,但由于它是草案的浓缩版本,最终进入了前面提到的书籍,因此对a的概念进行了一些重要的讨论.包和界面所有权是区分这一原则的关键,也是设计模式(Gamma等人)一书中关于"编程到界面,而不是实现"的更一般的建议.
为了提供总之,依赖倒置原则主要是关于反转从"更高的水平"组件具有依赖性的常规方向为"较低级"组件,使得"较低级"组件是依赖于接口拥有的"更高级别"的部件.(注意:这里的"更高级别"组件是指需要外部依赖/服务的组件,不一定是它在分层体系结构中的概念位置.)这样做,耦合不会因为从理论上从组件转移而减少太多对理论上更有价值的组件而言价值较低.
这是通过设计组件来实现的,这些组件的外部依赖性是根据组件的使用者必须提供实现的接口来表示的.换句话说,定义的接口表示组件所需的内容,而不是您如何使用组件(例如"INeedSomething",而不是"IDoSomething").
依赖性倒置原则没有涉及的是通过使用接口来抽象依赖性的简单实践(例如MyService→[ILogger⇐Logger]).虽然这将组件与依赖项的特定实现细节分离,但它不会反转使用者和依赖项之间的关系(例如[MyService→IMyServiceLogger]⇐Logger.
依赖倒置原则的重要性可以归结为一个单一的目标,即能够重用依赖于外部依赖性的软件组件来实现其功能的一部分(日志记录,验证等).
在这个重用的一般目标中,我们可以描述两种子类型的重用:
在具有子依赖项实现的多个应用程序中使用软件组件(例如,您已开发了DI容器并希望提供日志记录,但不希望将容器与特定记录器耦合,以便使用容器的每个人也必须使用您选择的日志库).
在不断变化的环境中使用软件组件(例如,您已经开发了业务逻辑组件,这些组件在实现细节不断发展的应用程序的多个版本中保持不变).
第一种情况是在多个应用程序之间重用组件,例如使用基础架构库,目标是为消费者提供核心基础结构需求,而不将消费者与您自己的库的子依赖关系联系起来,因为依赖于这些依赖关系需要您的消费者也需要相同的依赖关系.当您的库的用户选择使用不同的库来满足相同的基础架构需求时(例如NLog与log4net),或者他们选择使用与版本不向后兼容的所需库的更高版本时,这可能会出现问题您的图书馆需要.
第二种情况是重用业务逻辑组件(即"更高级别的组件"),目标是将应用程序的核心域实现与实现细节的不断变化的需求隔离开(即更改/升级持久性库,消息库) ,加密策略等).理想情况下,更改应用程序的实现细节不应该破坏封装应用程序业务逻辑的组件.
注意:有些人可能会反对将第二种情况描述为实际重用,并推断在单个不断发展的应用程序中使用的业务逻辑组件等组件仅代表一次使用.然而,这里的想法是,对应用程序的实现细节的每次更改都会呈现新的上下文,因此会有不同的用例,尽管最终目标可以区分为隔离与可移植性.
虽然在第二种情况下遵循依赖性倒置原则可以提供一些好处,但应该注意的是,它应用于现代语言(如Java和C#)的价值大大降低,可能达到无关紧要的程度.如前所述,DIP涉及将实现细节完全分离为单独的包.然而,在不断发展的应用程序的情况下,简单地利用根据业务域定义的接口将防止由于实现细节组件的需求的改变而需要修改更高级别的组件,即使实现细节最终在同一个包内.该原则的这一部分反映了当原则被编纂(即C++)与新语言无关时与语言相关的方面.也就是说,依赖性倒置原则的重要性主要在于可重用软件组件/库的开发.
可以在此处找到与接口的简单使用,依赖注入和分离接口模式相关的该原理的更长时间的讨论.
检查此文档:依赖性倒置原则.
它基本上说:
高级模块不应该依赖于低级模块.两者都应该依赖于抽象.
抽象不应该依赖于细节.细节应取决于抽象.
至于为什么它很重要,简而言之:变更是有风险的,并且通过依赖于概念而不是实施,您减少了呼叫站点的变更需求.
实际上,DIP减少了不同代码之间的耦合.我们的想法是,尽管有许多方法可以实现,例如,日志记录工具,但您使用它的方式应该是及时相对稳定的.如果您可以提取表示日志记录概念的接口,则此接口应该比其实现更加稳定,并且在维护或扩展该日志记录机制时,您可以进行的更改应该更少地影响调用站点.
通过使实现依赖于接口,您可以在运行时选择哪种实现更适合您的特定环境.根据具体情况,这也可能很有趣.
当我们设计软件应用程序时,我们可以认为低级别类实现基本和主要操作(磁盘访问,网络协议......)和高级类,这些类封装了复杂的逻辑(业务流,......).
最后一个依赖于低级别的课程.实现这种结构的一种自然方式是编写低级类,一旦我们让它们编写复杂的高级类.由于高级类是根据其他类别定义的,因此这似乎是合乎逻辑的方法.但这不是灵活的设计.如果我们需要更换低级别课程会怎样?
依赖性倒置原则指出:
高级模块不应该依赖于低级模块.两者都应该依赖于抽象.
抽象不应该依赖于细节.细节应取决于抽象.
该原则旨在"颠倒"传统观念,即软件中的高级模块应该依赖于较低级别的模块.这里,高级模块拥有由较低级模块实现的抽象(例如,决定接口的方法).因此,使较低级别的模块依赖于较高级别的模块.
对我来说,正式文章中描述的依赖性倒置原则实际上是一种错误的尝试,旨在提高本质上不太可重用的模块的可重用性,以及一种解决C++语言问题的方法.
C++中的问题是头文件通常包含私有字段和方法的声明.因此,如果高级C++模块包含低级模块的头文件,则它将取决于该模块的实际实现细节.显然,这不是一件好事.但这不是今天常用的更现代语言中的问题.
高级模块本质上比低级模块具有更少的可重用性,因为前者通常比后者具有更多的应用程序/上下文特定.例如,实现UI屏幕的组件具有最高级别,并且非常(完全?)特定于应用程序.尝试在不同的应用程序中重用这样的组件会产生反作用,并且只能导致过度工程化.
因此,只有当组件A真正有用于在不同的应用程序或上下文中重用时,才能在依赖于组件B(不依赖于A)的组件A的相同级别上创建单独的抽象.如果情况并非如此,那么应用DIP将是糟糕的设计.
良好应用的依赖性反转可在应用程序的整个体系结构级别提供灵活性和稳定性.它将使您的应用程序更安全,更稳定地发展.
传统上,分层架构UI依赖于业务层,而这依赖于数据访问层.
http://xurxodev.com/content/images/2016/02/Traditional-Layered.png
您必须了解图层,包或库.让我们看看代码将如何.
我们将拥有数据访问层的库或包.
// DataAccessLayer.dll public class ProductDAO { }
另一个依赖于数据访问层的库或包层业务逻辑.
// BusinessLogicLayer.dll using DataAccessLayer; public class ProductBO { private ProductDAO productDAO; }
依赖性倒置表示以下内容:
高级模块不应该依赖于低级模块.两者都应该取决于抽象.
抽象不应该依赖于细节.细节应取决于抽象.
什么是高级模块和低级别?诸如库或包之类的思维模块,高级模块将是那些传统上具有依赖性和低级别的模块.
换句话说,模块高级别将是调用操作的位置,而低级别是执行操作的位置.
从这个原则得出的一个合理的结论是,结构之间应该没有依赖关系,但必须依赖于抽象.但根据我们采取的方法,我们可能会误用投资依赖依赖,而是抽象.
想象一下,我们调整我们的代码如下:
我们将有一个用于定义抽象的数据访问层的库或包.
// DataAccessLayer.dll public interface IProductDAO public class ProductDAO : IProductDAO{ }
另一个依赖于数据访问层的库或包层业务逻辑.
// BusinessLogicLayer.dll using DataAccessLayer; public class ProductBO { private IProductDAO productDAO; }
虽然我们依赖于业务和数据访问之间的抽象依赖性仍然是相同的.
http://xurxodev.com/content/images/2016/02/Traditional-Layered.png
要获得依赖性反转,必须在此高级逻辑或域所在的模块或包中定义持久性接口,而不是在低级模块中.
首先定义域层是什么,并且通信的抽象定义为持久性.
// Domain.dll public interface IProductRepository; using DataAccessLayer; public class ProductBO { private IProductRepository productRepository; }
在持久层依赖于域之后,如果定义了依赖关系,则立即进行反转.
// Persistence.dll public class ProductDAO : IProductRepository{ }
http://xurxodev.com/content/images/2016/02/Dependency-Inversion-Layers.png
重要的是要充分理解这一概念,深化目的和利益.如果我们机械地停留并学习典型的案例库,我们将无法确定我们可以应用依赖原则的位置.
但为什么我们反转依赖?除了具体的例子之外,主要目标是什么?
这通常允许最不稳定的东西(不依赖于不太稳定的东西)更频繁地改变.
更改持久性类型更容易,数据库或技术访问与域逻辑相同的数据库或用于与持久性通信的操作.因此,依赖性是相反的,因为如果发生这种变化,更容易改变持久性.通过这种方式,我们不必更改域名.域层是最稳定的,这就是为什么它不应该依赖于任何东西.
但是,不仅有这个存储库示例.在许多场景中,该原则适用,并且存在基于该原理的架构.
存在依赖性反转是其定义的关键的体系结构.在所有域中,它是最重要的,它是抽象,表明域和其余的包或库之间的通信协议已定义.
在Clean架构中,域位于中心,如果您指向指示依赖关系的箭头方向,则很清楚哪些是最重要且最稳定的层.外层被认为是不稳定的工具,所以要避免依赖它们.
它与六边形体系结构的发生方式相同,其中域也位于中心部分,端口是从多米诺骨牌向外传递的抽象.在这里,很明显,域是最稳定的,传统的依赖性是倒置的.
基本上它说:
类应该依赖于抽象(例如接口,抽象类),而不是具体细节(实现).
这里的其他人已经给出了好的答案和好的例子.
究其原因DIP重要的是,因为它保证OO原则"松耦合的设计".
软件中的对象不应进入层次结构,其中某些对象是顶级对象,取决于低级对象.然后,低级对象的变化将波及到顶级对象,这使得软件变得非常脆弱.
您希望"顶级"对象非常稳定且不易变更,因此您需要反转依赖项.
陈述依赖性倒置原则的更明确的方法是:
封装复杂业务逻辑的模块不应直接依赖于封装业务逻辑的其他模块.相反,它们应该仅依赖于简单数据的接口.
即,不像Logic
人们通常那样实施你的课程:
class Dependency { ... } class Logic { private Dependency dep; int doSomething() { // Business logic using dep here } }
你应该做的事情如下:
class Dependency { ... } interface Data { ... } class DataFromDependency implements Data { private Dependency dep; ... } class Logic { int doSomething(Data data) { // compute something with data } }
Data
和DataFromDependency
应该住在同一个模块中Logic
,而不是Dependency
.
为什么这样?
这两个业务逻辑模块现在已经解耦.当Dependency
修改,你不需要改变Logic
.
理解什么Logic
是一个更简单的任务:它只在看起来像ADT的东西上运行.
Logic
现在可以更容易测试.您现在可以直接Data
使用虚假数据进行实例化并将其传入.无需模拟或复杂的测试脚手架.