什么是依赖倒置原则,为什么它很重要?
什么是依赖倒置原则,为什么它很重要?
检查这个文件: 依赖倒置原则 。
它基本上说:
- 高级模块不应该依赖于低级模块。 两者都应该取决于抽象。
- 抽象不应该依赖细节。 细节应该取决于抽象。
至于为什么它很重要,简而言之:变化是有风险的,通过依靠一个概念而不是一个实现,可以减less在呼叫站点的变化的需要。
有效地,DIP减less了不同代码之间的耦合。 这个想法是,尽pipe有许多方法可以实现,比如说一个日志工具,但是你使用它的方式应该是相对稳定的。 如果你可以提取一个表示日志logging概念的接口,那么这个接口应该比它的实现更加稳定,并且在维护或扩展这个日志logging机制的同时,调用站点应该受到更less的影响。
通过使实现依赖于接口,您可以在运行时select哪种实现更适合您的特定环境。 根据具体情况,这可能也很有趣。
C#中的敏捷软件开发,原理,模式和实践以及敏捷原则,模式和实践等书籍是充分理解依赖倒置原则背后的原始目标和动机的最佳资源。 “依存倒置原则”这个文章也是一个很好的资源,但是由于它是一个最终进入前面提到的书籍的草稿的精简版本,所以它忽略了关于包和接口的所有权,这是将这个原则与“devise模式”(Gamma等)一书中的“编程到接口,而不是实现”的更一般的build议区分开来的关键。
为了提供一个总结,依赖倒置原则主要是将传统的依赖关系从“较高级”组件转换到“较低级”组件,使得“较低级”组件依赖于“更高级”组件所拥有的接口。 (注意:这里的“更高级别”组件是指需要外部依赖/服务的组件,而不一定是在分层体系结构中的概念位置。)在这种情况下,耦合并没有减less太多,因为它从理论上的组件对于重用理论上对于重用更有价值的组件来说,价值较小。
这是通过devise组件的外部依赖关系来实现的,组件的使用者必须提供实现的接口。 换句话说,定义的接口表示组件所需要的,而不是你如何使用组件(例如“INeedSomething”,而不是“IDoSomething”)。
依赖倒置原理没有提到的是通过使用接口抽象依赖关系的简单实践(例如MyService→[ILogger⇐Logger])。 虽然这从一个组件的具体实现细节的依赖关系,它不反转消费者和依赖关系(例如[我的服务→IMyServiceLogger]⇐logging器之间的关系。
依赖倒置原则的重要性主要体现在依赖于外部依赖性(日志logging,validation等)的可重用软件组件的开发中,因为依赖这种依赖关系需要消费者也需要相同的依赖关系。 当你的程序库的用户select使用不同的库来满足相同的基础设施需求时(例如NLog和log4net),或者他们select使用与该版本不兼容的所需库的更高版本时,这可能会有问题您的图书馆需要。
这个原则的更长的讨论,因为它涉及到简单的接口,dependency injection和分离接口模式的使用可以在这里find。
对我而言,正如在官方文章中所描述的,依赖倒置原则实际上是一个错误的尝试,以增加本质上不太可重用的模块的可重用性,以及在C ++语言中解决问题的方法。
C ++中的问题是头文件通常包含私有字段和方法的声明。 因此,如果高级C ++模块包含低级模块的头文件,则将取决于该模块的实际实现细节。 而这显然不是一件好事。 但是这在今天常用的更现代的语言中不是问题。
高级模块本质上不如低级模块可重用,因为前者通常比后者更具有应用程序/上下文特定性。 例如,实现UI屏幕的组件具有最高级别,并且也非常(完全?)特定于应用程序。 试图在不同的应用程序中重用这样的组件是相反的,并且只能导致过度工程。
因此,只有组件A在不同的应用程序或上下文中重用时,才能在组件A的相同级别上创build依赖于组件B(不依赖于A)的组件A的独立抽象。 如果情况并非如此,那么应用DIP将是不好的devise。
基本上它说:
类应该依赖于抽象(如接口,抽象类),而不是具体的细节(实现)。
当我们devise软件应用程序时,我们可以考虑实现基本和主要操作的类(磁盘访问,networking协议…)和高级类(这些类封装了复杂的逻辑(业务stream,…))的低级类。
最后的依赖于低级别的类。 实现这种结构的一种自然的方式是编写低级的类,一旦我们有了它们来编写复杂的高级类。 由于高层次的类别是以其他方式来定义的,所以这似乎是合乎逻辑的方法。 但这不是一个灵活的devise。 如果我们需要更换一个低级别的课程会怎样?
依赖倒置原则指出:
- 高级模块不应该依赖于低级模块。 两者都应该取决于抽象。
- 抽象不应该取决于细节。 细节应该取决于抽象。
这个原则试图“颠倒”传统的观点,即软件中的高级模块应该依赖于低级模块。 这里高级模块拥有由低级模块实现的抽象(例如,决定接口的方法)。 从而使较低级别的模块依赖于较高级别的模块。
这里的其他人已经给出了很好的答案和好的例子。
DIP的重要性是因为它确保了OO原理的“松散耦合devise”。
软件中的对象不应该进入一个层次结构,其中一些对象是顶层对象,依赖于底层对象。 低级对象的变化会波及到顶级对象,这使得软件变得非常脆弱。
您希望您的“顶级”对象非常稳定,不易变更,因此您需要反转依赖关系。
很好地应用依赖性反转,在整个应用程序架构的层面上提供了灵活性和稳定性。 它将使您的应用程序更安全稳定地发展。
传统的分层build筑
传统上,分层体系结构UI取决于业务层,而这又取决于数据访问层。
http://xurxodev.com/contenthttp://img.dovov.com2016/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; }
尽pipe我们依赖于业务和数据访问之间的抽象依赖性依然如此。
http://xurxodev.com/contenthttp://img.dovov.com2016/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/contenthttp://img.dovov.com2016/02/Dependency-Inversion-Layers.png
深化原则
理解好概念,深化目的和效益是重要的。 如果我们坚持机械地学习典型的案例库,我们将无法确定哪里可以应用依赖性原则。
但是,为什么我们反转一个依赖? 具体例子之外的主要目标是什么?
这样通常可以让最稳定的事情,不依赖于不那么稳定的事情,更频繁地变化。
持久性types更容易更改,数据库或技术访问与域逻辑相同的数据库或devise为与持久性进行通信的操作。 因为这个原因,这种依赖关系是相反的,因为如果发生这种变化,更容易改变持久性。 这样我们就不用修改域名了。 领域层是最稳定的,这就是为什么它不应该依赖于任何东西。
但是,这不仅仅是这个存储库的例子。 这个原则适用的场景很多,有基于这个原则的架构。
架构
有依赖倒置是其定义的关键的体系结构。 在所有的领域,这是最重要的,这是抽象的,将表明域之间的通信协议,其余的包或库定义。
清洁的build筑
在干净的架构中 ,域位于中心,如果你看箭头的方向来表示依赖关系,那么很清楚什么是最重要和最稳定的层。 外层被认为是不稳定的工具,所以要避免依赖它们。
六angular形build筑
它与六边形结构的发生方式相同,其中域也位于中心部分,端口是从多米诺骨牌向外的通信抽象。 在这里再次certificate,这个领域是最稳定的,而传统的依赖关系则是倒转的。
陈述依赖倒置原则的更清晰的方法是:
封装复杂业务逻辑的模块不应该直接依赖封装业务逻辑的其他模块。 相反,他们只应该依靠简单数据的接口。
也就是说,而不是像人们通常那样实施你的课程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
现在可以更容易地testing。 您现在可以直接使用假数据实例化数据并将其传入。无需模拟或复杂的testing脚手架。
控制反转 (IoC)是一种devise模式,其中一个对象通过外部框架获取其依赖关系,而不是向框架请求依赖关系。
使用传统查找的伪代码示例:
class Service { Database database; init() { database = FrameworkSingleton.getService("database"); } }
使用IoC的类似代码:
class Service { Database database; init(database) { this.database = database; } }
IoC的好处是:
- 你不需要依赖中央框架,所以如果需要的话可以改变。
- 由于对象是通过注入创build的,最好使用接口,所以很容易创buildunit testing来代替对模拟版本的依赖关系。
- 解耦代码。
控制容器的倒置和 Martin Fowler 的dependency injection模式也是一个很好的解读。 我发现Head First Design Patterns是我第一次学习DI和其他模式的一本很棒的书。
依赖倒转的重点是制作可重用的软件。
这个想法是,而不是两个相互依赖的代码,他们依靠一些抽象的接口。 那么你可以重用任何一个没有其他的一块。
最常见的方式是通过像Java中的Spring这样的控制反转(IoC)容器。 在这个模型中,对象的属性是通过一个XMLconfiguration来设置的,而不是出来的对象,并find它们的依赖关系。
想象一下这个伪代码…
public class MyClass { public Service myService = ServiceLocator.service; }
MyClass直接依赖于Service类和ServiceLocator类。 如果你想在另一个应用程序中使用它,它需要这两个。 现在想象一下…
public class MyClass { public IService myService; }
现在,MyClass依赖于一个接口,IService接口。 我们会让IoC容器实际设置该variables的值。
所以现在,MyClass可以很容易地在其他项目中重复使用,而不需要将其他两个类的依赖关系一起使用。
更好的是,你不必拖拽MyService的依赖关系,依赖关系和依赖关系,那么你就明白了。