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


查看这个文档:依赖倒置原则。

它基本上是说:

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

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

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

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


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

其思想是,两段代码不再相互依赖,而是依赖于一些抽象的接口。然后你可以在没有另一块的情况下重复使用其中的任何一块。

最常见的实现方式是通过控制反转(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的依赖项,以及这些依赖项的依赖项,以及…好吧,你懂的。


控制反转(IoC)是一种设计模式,在这种模式下,对象通过外部框架获得其依赖项,而不是向框架请求其依赖项。

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

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

使用IoC的类似代码:

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

国际奥委会的好处是:

您不依赖于中心 框架,所以这可以改变如果 想要的。 因为对象是被创建的 以注射方式使用为佳 接口,很容易创建单元 替换依赖项的测试 模拟版本。 解耦代码。


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

DIP之所以重要,是因为它保证了oo原则的“松散耦合设计”。

软件中的对象不应该进入一个层次结构,其中一些对象是顶级对象,依赖于低级对象。底层对象的变化会波及到顶层对象,这使得软件非常容易发生变化。

你希望你的“顶级”对象非常稳定,不容易改变,因此你需要颠倒依赖关系。


这是什么?

The books Agile Software Development, Principles, Patterns, and Practices and Agile Principles, Patterns, and Practices in C# are the best resources for fully understanding the original goals and motivations behind the Dependency Inversion Principle. The article "The Dependency Inversion Principle" is also a good resource, but due to the fact that it is a condensed version of a draft which eventually made its way into the previously mentioned books, it leaves out some important discussion on the concept of a package and interface ownership which are key to distinguishing this principle from the more general advise to "program to an interface, not an implementation" found within the book Design Patterns (Gamma, et. al).

To provide a summary, the Dependency Inversion Principle is primarily about reversing the conventional direction of dependencies from "higher level" components to "lower level" components such that "lower level" components are dependent upon the interfaces owned by the "higher level" components. (Note: "higher level" component here refers to the component requiring external dependencies/services, not necessarily its conceptual position within a layered architecture.) In doing so, coupling isn't reduced so much as it is shifted from components that are theoretically less valuable to components which are theoretically more valuable.

这是通过设计其外部依赖关系用接口表示的组件来实现的,该接口的实现必须由组件的使用者提供。换句话说,定义的接口表示组件需要什么,而不是如何使用组件。“INeedSomething”,而不是“IDoSomething”)。

依赖倒置原则没有提到的是通过使用接口(例如MyService→[ILogger⇐Logger])抽象依赖关系的简单实践。虽然这将组件从依赖项的特定实现细节中解耦,但它并没有反转使用者和依赖项之间的关系(例如[MyService→IMyServiceLogger]⇐Logger。

为什么它很重要?

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

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

Using a software component within multiple applications with sub-dependency implementations (e.g. You've developed a DI container and want to provide logging, but don't want to couple your container to a specific logger such that everyone that uses your container has to also use your chosen logging library). Using software components within an evolving context (e.g. You've developed business-logic components which remain the same across multiple versions of an application where the implementation details are evolving).

With the first case of reusing components across multiple applications, such as with an infrastructure library, the goal is to provide a core infrastructure need to your consumers without coupling your consumers to sub-dependencies of your own library since coupling to such dependencies requires your consumers to require the same dependencies as well. This can be problematic when consumers of your library choose to use a different library for the same infrastructure needs (e.g. NLog vs. log4net), or if they choose to use a later version of the required library which isn't backward compatible with the version required by your library.

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

注意:有些人可能反对将第二种情况描述为实际的重用,认为在单个演进应用程序中使用的业务逻辑组件等组件只代表单一用途。然而,这里的思想是,对应用程序实现细节的每次更改都呈现一个新的上下文,因此呈现一个不同的用例,尽管最终目标可以区分为隔离和可移植性。

While following the Dependency Inversion Principle in this second case can offer some benefit, it should be noted that its value as applied to modern languages such as Java and C# is much reduced, perhaps to the point of being irrelevant. As discussed earlier, the DIP involves separating implementation details into separate packages completely. In the case of an evolving application, however, simply utilizing interfaces defined in terms of the business domain will guard against needing to modify higher-level components due to changing needs of implementation detail components, even if the implementation details ultimately reside within the same package. This portion of the principle reflects aspects that were pertinent to the language in view when the principle was codified (i.e. C++) which aren't relevant to newer languages. That said, the importance of the Dependency Inversion Principle primarily lies with the development of reusable software components/libraries.

在这里可以找到关于这个原则的更长的讨论,因为它与接口的简单使用、依赖注入和分离接口模式有关。此外,关于该原则如何与动态类型语言(如JavaScript)相关的讨论可以在这里找到。


对我来说,官方文章中所描述的依赖倒置原则实际上是一种错误的尝试,它试图提高固有的可重用性较低的模块的可重用性,同时也是一种解决c++语言中的问题的方法。

c++中的问题是头文件通常包含私有字段和方法的声明。因此,如果高级c++模块包含低级模块的头文件,它将取决于该模块的实际实现细节。显然,这不是一件好事。但在今天常用的更现代的语言中,这不是一个问题。

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

因此,在组件a的同一级别上创建依赖于组件B(不依赖于组件a)的单独抽象,只有在组件a确实对在不同的应用程序或上下文中重用有用的情况下才能完成。如果不是这样,那么应用DIP将是糟糕的设计。


基本上它说:

类应该依赖于抽象(例如接口,抽象类),而不是特定的细节(实现)。


依赖倒置的良好应用在应用程序的整个体系结构层面上提供了灵活性和稳定性。它将允许您的应用程序更安全、更稳定地发展。

传统分层建筑

传统上,分层架构UI依赖于业务层,而业务层又依赖于数据访问层。

您必须了解层、包或库。让我们看看代码是怎样的。

我们将有一个用于数据访问层的库或包。

// 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)

深化原则

重要的是要吸收好概念,深化目的和效益。如果我们机械地停留在学习典型案例存储库中,我们将无法确定在哪里可以应用依赖原则。

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

这通常允许最稳定的东西,不依赖于不太稳定的东西,更频繁地变化。

持久化类型的更改(访问相同数据库的数据库或技术)比设计用于与持久化通信的域逻辑或操作更容易。因此,这种依赖是相反的,因为如果发生这种更改,则更容易更改持久性。这样我们就不需要改变定义域了。域层是所有层中最稳定的,这就是为什么它不应该依赖于任何东西。

但不只是这个存储库示例。有许多应用此原则的场景,并且有基于此原则的架构。

体系结构

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

干净的建筑

在Clean体系结构中,域位于中心,如果您查看指示依赖关系的箭头方向,就可以清楚地看到哪些是最重要和最稳定的层。外层被认为是不稳定的工具,因此避免依赖它们。

(来源:8 thlight.com)

六角结构

这与六边形体系结构发生的情况相同,其中域也位于中心部分,端口是从多米诺骨牌向外通信的抽象。这里很明显,域是最稳定的,传统的依赖关系是颠倒的。

(来源:pragprog.com)


依赖倒置原则的一个更清晰的表述方式是:

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

也就是说,不是像人们通常做的那样实现你的类逻辑:

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的东西起作用。 现在可以更容易地检验逻辑。现在,您可以直接实例化假数据Data并将其传入。不需要模拟或复杂的测试脚手架。


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

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

依赖倒置原则指出:

高级模块不应该依赖于低级模块。两者都应该依赖于抽象。 抽象不应该依赖于细节。细节应该依赖于抽象。

这个原则试图“颠倒”传统的概念,即软件中的高级模块应该依赖于低级模块。在这里,高级模块拥有由低级模块实现的抽象(例如,决定接口的方法)。因此,较低级别的模块依赖于较高级别的模块。


依赖倒置原则(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();
        }
    }

注意:类应该依赖于抽象,如接口或抽象类,而不是特定的细节(接口的实现)。


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

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

以下是一些关于这方面的好文章:

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

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

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


除了一大堆不错的答案之外,我还想举一个我自己的小例子来说明好的和坏的做法。是的,我不是那种会扔石头的人!

比如说,你想要一个小程序通过控制台I/O将一个字符串转换成base64格式。这是一种幼稚的方法:

class Program
{
    static void Main(string[] args)
    {
        /*
         * BadEncoder: High-level class *contains* low-level I/O functionality.
         * Hence, you'll have to fiddle with BadEncoder whenever you want to change
         * the I/O mode or details. Not good. A good encoder should be I/O-agnostic --
         * problems with I/O shouldn't break the encoder!
         */
        BadEncoder.Run();            
    }
}

public static class BadEncoder
{
    public static void Run()
    {
        Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(Console.ReadLine())));
    }
}    

DIP基本上是说高级组件不应该依赖于低级实现,根据Robert C. Martin(《清洁架构》)的说法,“级别”是指与I/O的距离。但是如何摆脱这种困境呢?简单地让中央编码器只依赖于接口,而不考虑这些接口是如何实现的:

class Program
{
    static void Main(string[] args)
    {           
        /* Demo of the Dependency Inversion Principle (= "High-level functionality
         * should not depend upon low-level implementations"): 
         * You can easily implement new I/O methods like
         * ConsoleReader, ConsoleWriter without ever touching the high-level
         * Encoder class!!!
         */            
        GoodEncoder.Run(new ConsoleReader(), new ConsoleWriter());        }
}

public static class GoodEncoder
{
    public static void Run(IReadable input, IWriteable output)
    {
        output.WriteOutput(Convert.ToBase64String(Encoding.ASCII.GetBytes(input.ReadInput())));
    }
}

public interface IReadable
{
    string ReadInput();
}

public interface IWriteable
{
    void WriteOutput(string txt);
}

public class ConsoleReader : IReadable
{
    public string ReadInput()
    {
        return Console.ReadLine();
    }
}

public class ConsoleWriter : IWriteable
{
    public void WriteOutput(string txt)
    {
        Console.WriteLine(txt);
    }
}

注意,你不需要触摸GoodEncoder来改变I/O模式——这个类对它知道的I/O接口很满意;任何IReadable和IWriteable的低级实现都不会打扰它。


如果我们可以假定公司的“高级”员工是通过执行他们的计划来获得报酬的,并且这些计划是由许多“低级”员工的计划的综合执行来交付的,那么我们可以说,如果高级员工的计划描述以任何方式与任何低级员工的具体计划耦合在一起,那么这通常是一个糟糕的计划。

If a high level executive has a plan to "improve delivery time", and indicates that an employee in the shipping line must have coffee and do stretches each morning, then that plan is highly coupled and has low cohesion. But if the plan makes no mention of any specific employee, and in fact simply requires "an entity that can perform work is prepared to work", then the plan is loosely coupled and more cohesive: the plans do not overlap and can easily be substituted. Contractors, or robots, can easily replace the employees and the high level's plan remains unchanged.

“高级”在依赖倒置原则中意味着“更重要”。


我可以在上面的回答中看到很好的解释。然而,我想用简单的例子提供一些简单的解释。

依赖倒置原则允许程序员删除硬编码的依赖项,这样应用程序就变得松散耦合和可扩展。

如何实现这一点:通过抽象

没有依赖反转:

 class Student {
    private Address address;

    public Student() {
        this.address = new Address();
    }
}
class Address{
    private String perminentAddress;
    private String currentAdrress;

    public Address() {
    }
} 

在上面的代码片段中,地址对象是硬编码的。相反,如果我们可以使用依赖倒置,并通过传递构造函数或setter方法注入地址对象。让我们来看看。

使用依赖倒置:

class Student{
    private Address address;

    public Student(Address address) {
        this.address = address;
    }
    //or
    public void setAddress(Address address) {
        this.address = address;
    }
}

依赖倒置原理(DIP)

它是SOLID[About]的一部分,SOLID[About]是OOD的一部分,由Bob叔叔介绍。它是关于类(层…)之间的松散耦合。类不应该依赖于具体的实现,类应该依赖于抽象/接口

问题:

//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依赖于B实现的接口IB,这意味着A依赖于IB的多重实现(一对多)

[DIP vs DI vs IoC]