什么是依赖倒置原则以及为什么它很重要?

有帮助吗?

解决方案

查看此文档: 依赖倒置原则.

它基本上说:

  • 高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
  • 抽象永远不应该依赖于细节。细节应该取决于抽象。

至于为什么它很重要,简而言之:更改是有风险的,通过依赖概念而不是实现,您可以减少在调用站点进行更改的需要。

DIP 有效地减少了不同代码段之间的耦合。这个想法是,虽然有很多实现(例如,日志记录工具)的方法,但是您使用它的方式应该在时间上相对稳定。如果您可以提取一个代表日志记录概念的接口,那么该接口在时间上应该比其实现更加稳定,并且调用站点受您在维护或扩展该日志记录机制时所做的更改的影响应该小得多。

通过使实现依赖于接口,您可以在运行时选择更适合您的特定环境的实现。根据具体情况,这可能也很有趣。

其他提示

《敏捷软件开发、原则、模式和实践》和《C# 中的敏捷原则、模式和实践》一书是充分理解依赖倒置原则背后的原始目标和动机的最佳资源。《依赖倒置原则》一文也是一个很好的资源,但由于它是最终进入前面提到的书籍的草稿的浓缩版本,因此它遗漏了一些关于依赖倒置概念的重要讨论。包和接口所有权是区分这一原则与《设计模式》(Gamma 等人)一书中“针对接口编程,而不是实现”的更一般建议的关键。等)。

总结一下,依赖倒置原则主要是关于 倒车 从“较高级别”组件到“较低级别”组件的传统依赖方向,使得“较低级别”组件依赖于接口 拥有的 由“更高级别”组件组成。(笔记:这里的“更高级别”组件是指需要外部依赖项/服务的组件,不一定是其在分层架构中的概念位置。)这样做时,耦合不是 减少 就这样 转移 从理论上价值较低的组件到理论上价值较高的组件。

这是通过设计组件来实现的,这些组件的外部依赖性以接口的形式表达,而组件的使用者必须为其提供实现。换句话说,定义的接口表达了组件需要什么,而不是如何使用组件(例如“INeedSomething”,而不是“IDoSomething”)。

依赖倒置原则没有指的是通过使用接口(例如,MyService → [ILogger ⇐ 记录器])。虽然这将组件与依赖项的特定实现细节解耦,但它并没有颠倒消费者和依赖项之间的关系(例如,[MyService → IMyServiceLogger] ⇐ 记录器。

依赖倒置原则的重要性可以归结为一个单一目标,即能够重用依赖外部依赖来实现其部分功能(日志记录、验证等)的软件组件。

在重用的总体目标内,我们可以划分重用的两个子类型:

  1. 在具有子依赖性实现的多个应用程序中使用软件组件(例如您已经开发了一个 DI 容器并希望提供日志记录,但不希望将您的容器耦合到特定的记录器,这样使用您的容器的每个人也必须使用您选择的日志记录库)。

  2. 在不断发展的环境中使用软件组件(例如您开发了业务逻辑组件,这些组件在实现细节不断发展的应用程序的多个版本中保持不变)。

对于跨多个应用程序重用组件的第一种情况(例如使用基础设施库),目标是为您的消费者提供核心基础设施需求,而不将您的消费者与您自己的库的子依赖项耦合,因为对此类依赖项的依赖需要您消费者也需要相同的依赖关系。当您的图书馆的使用者选择使用不同的图书馆来满足相同的基础设施需求时(例如,NLog 对比log4net),或者如果他们选择使用所需库的更高版本,该版本与您的库所需的版本不向后兼容。

对于重用业务逻辑组件的第二种情况(即“更高级别的组件”),目标是将应用程序的核心域实现与实现细节不断变化的需求隔离(即更改/升级持久性库、消息传递库、加密策略等)。理想情况下,更改应用程序的实现细节不应破坏封装应用程序业务逻辑的组件。

笔记:有些人可能反对将第二种情况描述为实际重用,理由是在单个不断发展的应用程序中使用的诸如业务逻辑组件之类的组件仅代表单次使用。然而,这里的想法是,对应用程序实现细节的每次更改都会呈现一个新的上下文,从而呈现一个不同的用例,尽管最终目标可以区分为隔离与集成。可移植性。

虽然在第二种情况下遵循依赖倒置原则可以带来一些好处,但应该注意的是,它应用于 Java 和 C# 等现代语言的价值大大降低,甚至可能到了无关紧要的程度。如前所述,DIP 涉及将实现细节完全分离到单独的包中。然而,在不断发展的应用程序的情况下,简单地利用根据业务领域定义的接口将防止由于实现细节组件的需求变化而需要修改更高级别的组件,即使实现细节最终在同一个包中。原则的这一部分反映了与原则编纂时所考虑的语言相关的方面(即,C++),与较新的语言无关。也就是说,依赖倒置原则的重要性主要在于可重用软件组件/库的开发。

有关此原则的详细讨论,因为它涉及接口的简单使用、依赖注入和分离接口模式,请参见 这里. 。此外,还可以找到有关该原理如何与 JavaScript 等动态类型语言相关的讨论 这里.

当我们设计软件应用程序时,我们可以考虑低级类,即实现基本和主要操作(磁盘访问、网络协议等)的类,以及高级类,即封装复杂逻辑(业务流,...)的类。

最后一个依赖于低级别的课程。实现这种结构的一种自然方法是编写低级类,一旦我们有了它们就可以编写复杂的高级类。由于高级类是根据其他类定义的,这似乎是合乎逻辑的方法。但这不是一个灵活的设计。如果我们需要替换低级别的类会发生什么?

依赖倒置原则指出:

  • 高层模块不应依赖于低层模块。两者都应该依赖于抽象。
  • 抽象不应该依赖于细节。细节应该取决于抽象。

该原则旨在“反转”软件中的高级模块应依赖于较低级别模块的传统观念。这里高层模块拥有由低层模块实现的抽象(例如,决定接口的方法)。从而使较低级别的模块依赖于较高级别的模块。

对我来说,依赖倒置原则,如 官方文章, ,实际上是一种误导性的尝试,旨在提高本质上可重用性较差的模块的可重用性,同时也是解决 C++ 语言中的问题的一种方法。

C++ 中的问题是头文件通常包含私有字段和方法的声明。因此,如果高级 C++ 模块包含低级模块的头文件,则取决于实际情况 执行 该模块的详细信息。显然,这不是一件好事。但对于当今常用的更现代的语言来说,这不是问题。

高级模块本质上比低级模块可重用性差,因为前者通常比后者更特定于应用程序/上下文。例如,实现 UI 屏幕的组件是最高级别的,并且也非常(完全?)特定于应用程序。尝试在不同的应用程序中重用此类组件会适得其反,并且只会导致过度设计。

因此,只有当组件 A 确实可在不同应用程序或上下文中重用时,才能在组件 A 的同一级别创建依赖于组件 B(不依赖于 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

深化原理

重要的是要很好地理解这个概念,深化其目的和好处。如果我们机械地停留在典型案例库中,我们将无法确定在哪里可以应用依赖原则。

但为什么我们要反转依赖关系呢?除了具体例子之外,主要目标是什么?

常见的有这样的 允许最稳定的事物(不依赖于不太稳定的事物)更频繁地改变。

与设计用于与持久性通信的域逻辑或操作相比,更改持久性类型(无论是数据库还是访问同一数据库的技术)更容易。因此,依赖性被逆转,因为如果发生这种变化,更容易改变持久性。这样我们就不必更改域。领域层是最稳定的,这就是为什么它不应该依赖于任何东西。

但不仅仅是这个存储库示例。这个原则适用的场景有很多,也有基于这个原则的架构。

架构

在某些架构中,依赖倒置是其定义的关键。在所有域中,它是最重要的,它是抽象,它将指示域与定义的其余包或库之间的通信协议。

干净的架构

干净的架构 该域位于中心,如果您沿着指示依赖关系的箭头方向看,很清楚什么是最重要和最稳定的层。外层被认为是不稳定的工具,因此请避免依赖它们。

六边形架构

六边形架构的情况也是如此,其中域也位于中心部分,端口是多米诺骨牌向外通信的抽象。在这里,很明显,领域是最稳定的,传统的依赖关系是相反的。

基本上它说:

类应该依赖于抽象(例如接口、抽象类),而不是具体细节(实现)。

表述依赖倒置原则的更清晰的方法是:

封装复杂业务逻辑的模块不应直接依赖于封装业务逻辑的其他模块。相反,它们应该仅依赖于简单数据的接口。

即,而不是实现你的类 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
    }
}

DataDataFromDependency 应该与以下模块位于同一模块中 Logic, ,不与 Dependency.

为什么要这样做?

  1. 两个业务逻辑模块现已解耦。什么时候 Dependency 改变,你不需要改变 Logic.
  2. 了解什么 Logic 确实是一个简单得多的任务:它只在看起来像 ADT 的东西上运行。
  3. Logic 现在可以更容易地进行测试。您现在可以直接实例化 Data 带有虚假数据并将其传递进去。不需要模拟或复杂的测试脚手架。

其他人已经在这里给出了很好的答案和很好的例子。

原因 重要的是因为它确保了 OO 原则“松耦合设计”。

软件中的对象不应进入层次结构,其中某些对象是顶级对象,依赖于低级对象。然后,低级对象的更改将波及到顶级对象,这使得软件对于更改非常脆弱。

您希望您的“顶级”对象非常稳定并且不易发生变化,因此您需要反转依赖关系。

控制反转 (IoC) 是一种设计模式,其中对象由外部框架传递其依赖项,而不是向框架询问其依赖项。

使用传统查找的伪代码示例:

class Service {
    Database database;
    init() {
        database = FrameworkSingleton.getService("database");
    }
}

使用 IoC 的类似代码:

class Service {
    Database database;
    init(database) {
        this.database = database;
    }
}

IoC 的好处是:

  • 您对中央框架不依赖,因此可以根据需要进行更改。
  • 由于对象是通过注入创建的,最好是使用接口,因此很容易创建用模拟版本代替依赖项的单元测试。
  • 解耦代码。

依赖倒置的目的是制作可重用的软件。

这个想法是,它们不是依赖于彼此的两段代码,而是依赖于一些抽象的接口。然后您可以重复使用其中一个而不需要另一个。

最常见的实现方式是通过控制反转 (IoC) 容器,例如 Java 中的 Spring。在这个模型中,对象的属性是通过 XML 配置来设置的,而不是对象出去寻找它们的依赖关系。

想象一下这个伪代码......

public class MyClass
{
  public Service myService = ServiceLocator.service;
}

MyClass 直接依赖于 Service 类和 ServiceLocator 类。如果您想在另一个应用程序中使用它,则需要这两者。现在想象一下这...

public class MyClass
{
  public IService myService;
}

现在,MyClass 依赖于一个接口,即 IService 接口。我们会让 IoC 容器实际设置该变量的值。

因此,现在 MyClass 可以轻松地在其他项目中重用,而无需带来其他两个类的依赖关系。

更好的是,您不必拖动 MyService 的依赖项,以及这些依赖项的依赖项,以及...反正你懂这个意思。

控制容器反转和依赖注入模式 马丁·福勒 (Martin Fowler) 的著作也很值得一读。我发现 首先设计模式 这是一本很棒的书,适合我第一次学习 DI 和其他模式。

依赖倒置:依赖于抽象,而不是具体。

控制反转:主要与抽象,以及主要如何成为系统的粘合剂。

DIP and IoC

以下是一些讨论此问题的好帖子:

https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/

https://coderstower.com/2019/04/02/main-and-abstraction-the-de Coupled-peers/

https://coderstower.com/2019/04/09/inversion-of-control-putting-all-together/

依赖倒置原则(DIP)说

i) 高层模块不应依赖于低层模块。两者都应该依赖于抽象。

ii) 抽象永远不应该依赖于细节。细节应该取决于抽象。

例子:

    public interface ICustomer
    {
        string GetCustomerNameById(int id);
    }

    public class Customer : ICustomer
    {
        //ctor
        public Customer(){}

        public string GetCustomerNameById(int id)
        {
            return "Dummy Customer Name";
        }
    }

    public class CustomerFactory
    {
        public static ICustomer GetCustomerData()
        {
            return new Customer();
        }
    }

    public class CustomerBLL
    {
        ICustomer _customer;
        public CustomerBLL()
        {
            _customer = CustomerFactory.GetCustomerData();
        }

        public string GetCustomerNameById(int id)
        {
            return _customer.GetCustomerNameById(id);
        }
    }

    public class Program
    {
        static void Main()
        {
            CustomerBLL customerBLL = new CustomerBLL();
            int customerId = 25;
            string customerName = customerBLL.GetCustomerNameById(customerId);


            Console.WriteLine(customerName);
            Console.ReadKey();
        }
    }

笔记:类应该依赖于接口或抽象类等抽象,而不是具体的细节(接口的实现)。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top