《敏捷软件开发》、《敏捷原则模式与实践》和《C#敏捷原则、模式与实践》这些书是深入理解依赖倒置原则背后最初的目标和动机的最佳资源。文章“依赖倒置原则”也是一个很好的资源,但由于它是草案的简化版本,最终进入了前面提到的书中,因此它遗漏了一些重要的讨论,涉及到包和接口所有权的概念,这些概念是区分这个原则与书籍《设计模式》(Gamma等人)中找到的更普遍的建议“编程到接口而不是实现”的关键。
总之,依赖倒置原则主要是为了将依赖项的传递方向从“高层”组件反转到“低层”组件,使得“低层”组件依赖于“高层”组件拥有的接口。(注意:这里的“高层”组件指需要外部依赖/服务的组件,不一定是它在分层架构中的概念位置。)通过这样做,耦合度不仅仅是减少,而是从理论上价值较低的组件转移到理论上更有价值的组件。
这是通过设计组件来实现的,其外部依赖项是以消费者提供的实现所需要的接口来表达的。换句话说,定义的接口表达了组件需要的内容,而不是你如何使用组件(例如“INeedSomething”而不是“IDoSomething”)。
依赖倒置原则不涉及抽象化依赖项的简单实践,例如通过接口的使用将依赖项抽象化(例如MyService →[ILogger ⇐ Logger])。虽然这将组件从依赖项的特定实现细节中解耦,但它并没有颠倒使用者和依赖项之间的关系(例如[MyService →IMyServiceLogger] ⇐ Logger)。
虽然在第二种情况下遵循依赖倒置原则可以带来一些好处,但需要注意的是,对于像Java和C#这样的现代语言,其价值大大降低,甚至可能变得不相关。如前所述,DIP涉及将实现细节完全分离到单独的包中。然而,在不断发展的应用程序中,仅使用基于业务领域定义的接口将防止由于实现细节组件的不断变化而需要修改高层组件,即使实现细节最终驻留在同一个包中。该原则的此部分反映了原则制定时(即C ++)与语言相关的方面,但对于新语言并不相关。即便如此,依赖倒置原则的重要性主要在于可重复使用的软件组件/库的开发。
有关此原则如何与简单使用接口、依赖注入和分离接口模式相关的更长讨论可以在这里找到。此外,有关该原则如何与JavaScript等动态类型语言相关的讨论可以在这里找到。
查看此文件:依赖倒置原则。
该原则基本上指出:
至于为什么这很重要,简而言之:更改是有风险的,通过依赖于一个概念而不是实现,您可以减少调用站点需要进行更改的程度。
有效地,DIP减少了不同代码段之间的耦合。其想法是,虽然有许多实现日志记录设施的方法,但您使用它的方式应该在时间上相对稳定。如果您可以提取代表日志记录概念的接口,则该接口应该比其实现更加稳定,并且调用站点应该受到维护或扩展该记录机制时所做更改的影响较小。
通过使实现也依赖于接口,您有可能在运行时选择更适合特定环境的实现。根据情况,这也可能很有趣。
// 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;
}
尽管我们依赖于业务和数据访问之间的抽象依赖关系,但它们之间的联系仍然保持不变。
要实现依赖反转,持久化接口必须在高层逻辑或领域所属的模块或包中定义,而不是在低层模块中定义。// Domain.dll
public interface IProductRepository;
using DataAccessLayer;
public class ProductBO {
private IProductRepository productRepository;
}
当持久层依赖于域时,如果定义了依赖项,则现在需要反转。
// Persistence.dll
public class ProductDAO : IProductRepository{
}
(来源: xurxodev.com)
重要的是要充分吸收这个概念,加深其目的和好处。如果我们只停留在机械地学习典型案例库中,就无法确定我们可以在哪里应用依赖原则。
但是为什么要反转依赖关系?除了具体的例子之外,主要目标是什么?
这通常允许最稳定的事物(不依赖于不稳定的事物)更频繁地发生变化。
更改持久性类型(数据库或访问同一数据库的技术)比更改与持久性通信的域逻辑或操作更容易。因此,依赖关系被反转,因为如果发生这种变化,更改持久性将更容易。这样,我们就不必更改域。域层是最稳定的,因此不应依赖任何东西。
除了这个存储库示例之外,还有许多场景适用于这个原则,并且有基于这个原则的架构。
有些架构中,依赖反转是定义的关键。在所有领域中,抽象是最重要的,它们将指示域与其余包或库之间的通信协议。
在干净架构中,域位于中心,如果您朝着表示依赖关系的箭头方向看,就清楚了哪些层最重要和稳定。外部层被认为是不稳定的工具,因此应避免依赖它们。
(来源:8thlight.com)
当我们设计软件应用程序时,可以将低级类视为实现基本和主要操作(磁盘访问、网络协议等)的类,将高级类视为封装复杂逻辑(业务流程等)的类。
后者依赖于低级类。一种自然的实现方式是先编写低级类,然后再编写复杂的高级类。由于高级类是根据其他类定义的,因此这似乎是合乎逻辑的方法。但这不是一种灵活的设计。如果我们需要替换低级类会发生什么呢?
依赖倒置原则规定:
该原则旨在“颠覆”软件中高级模块应该依赖于低级模块的传统概念。在这里,高级模块拥有抽象(例如,决定接口方法),这些抽象由低级模块实现。从而使低级模块依赖于高级模块。
基本上,它的意思是:
类应该依赖于抽象(例如接口、抽象类),而不是具体细节(实现)。
依赖倒置原则的更加明确的表述应该是:
封装复杂业务逻辑的模块不应直接依赖于封装业务逻辑的其他模块,而是应该仅依赖于简单数据的接口。
即,不要像人们通常所做的那样实现类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
并传递它。无需使用模拟对象或复杂的测试脚手架。DataFromDependency
直接引用Dependency
,并且在同一个模块中作为Logic
,那么Logic
模块仍然在编译时直接依赖于Dependency
模块。根据Uncle Bob解释的原则,避免这种情况正是DIP的全部意义。相反,为了遵循DIP,Data
应该与Logic
在同一个模块中,但DataFromDependency
应该与Dependency
在同一个模块中。 - Mark AmeryDataFromDependency
和 Data
应该确实定义在与 Logic
不同的模块中,但它们绝对不应该放在与 Dependency
相同的模块中。这通常是不可能的,例如当 Dependency
来自外部库时。 - mattvonb其他人在这里已经给出了好的答案和范例。
依赖倒置原则(DIP)之所以重要,是因为它确保了面向对象编程中的“松耦合设计”原则。
您的软件中的对象不应该形成层次结构,其中一些对象是依赖于低级对象的顶级对象。低级对象的更改将会传递到顶级对象,这会使软件非常脆弱,难以进行更改。
您希望您的“顶级”对象非常稳定,不易受到更改的影响,因此需要反转依赖关系。
依赖倒置原则(DIP)
DIP是SOLID原则的一部分[关于],也是面向对象设计(OOD)的一部分,由Uncle Bob提出。它主要关注类(layer...)之间的松耦合。类不应该依赖于具体实现,而应该依赖于抽象/接口
问题:
//A -> B
class A {
B b
func foo() {
b = B();
}
}
//A -> IB <|- B
//client[A -> IB] <|- B is the Inversion
class A {
IB ib // An abstraction between High level module A and low level module B
func foo() {
ib = B()
}
}
现在A
不再依赖于B
(一对一),现在A
依赖于接口IB
,这个接口由B
实现,这意味着A
依赖于IB
的多个实现(一对多)
控制反转(IoC)是一种设计模式,其中对象通过外部框架获取其依赖项,而不是向框架请求其依赖项。
使用传统查找的伪代码示例:
class Service {
Database database;
init() {
database = FrameworkSingleton.getService("database");
}
}
使用IoC的相似代码:
class Service {
Database database;
init(database) {
this.database = database;
}
}
控制反转(IoC)的好处包括: