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

时间:2008-09-15 12:53:20

标签: oop solid-principles glossary principles dependency-inversion

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

18 个答案:

答案 0 :(得分:134)

答案 1 :(得分:102)

请查看此文档:The Dependency Inversion Principle

基本上说:

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

至于为什么它很重要,简而言之:变更是有风险的,并且通过依赖于概念而不是实施,您可以减少呼叫站点的变更需求。

实际上,DIP减少了不同代码之间的耦合。我们的想法是,尽管有许多方法可以实现,例如,日志记录工具,但您使用它的方式应该是及时相对稳定的。如果您可以提取表示日志记录概念的接口,则此接口应该比其实现更加稳定,并且调用站点应该更少受到在维护或扩展该日志记录机制时可能进行的更改的影响。

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

答案 2 :(得分:11)

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

最后的依赖于低级别的课程。实现这种结构的一种自然方式是编写低级类,一旦我们让它们编写复杂的高级类。由于高级类是根据其他类别定义的,因此这似乎是合乎逻辑的方法。但这不是灵活的设计。如果我们需要更换低级别课程会怎样?

依赖性倒置原则指出:

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

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

答案 3 :(得分:9)

对我而言,official article中描述的依赖性倒置原则实际上是一种错误的尝试,旨在提高本质上不太可重用的模块的可重用性,以及解决C ++中的问题的方法。语言。

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

高级模块本质上比低级模块具有更少的可重用性,因为前者通常比后者具有更多的应用程序/上下文特定。例如,实现UI屏幕的组件具有最高级别,并且非常(完全?)特定于应用程序。尝试在不同的应用程序中重用这样的组件会产生反作用,并且只会导致过度工程化。

因此,只有当组件A真正用于在不同应用程序中重用时,才能在依赖于组件B(不依赖于A)的组件A的同一级别上创建单独的抽象。上下文。如果情况并非如此,那么应用DIP将是糟糕的设计。

答案 4 :(得分:9)

良好应用的依赖性反转可在应用程序的整个体系结构级别提供灵活性和稳定性。它将使您的应用程序更安全,更稳定地发展。

传统的分层架构

传统上,分层架构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 architecture中,域位于中心,如果您指向指示依赖关系的箭头方向,则很清楚哪些是最重要且最稳定的层。外层被认为是不稳定的工具,所以要避免依赖它们。

六角形建筑

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

答案 5 :(得分:8)

基本上它说:

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

答案 6 :(得分:5)

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

DIP之所以重要,是因为它确保了OO原则“松耦合设计”。

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

您希望“顶级”对象非常稳定且不易变更,因此您需要反转依赖项。

答案 7 :(得分:5)

更明确地说明依赖性倒置原则是:

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

即,不像人们通常那样实施您的课程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. 现在可以更轻松地测试
  4. Logic。您现在可以使用伪数据直接实例化Data并将其传入。无需模拟或复杂的测试脚手架。

答案 8 :(得分:3)

Inversion of control(IoC)是一种设计模式,其中一个对象通过外部框架传递其依赖性,而不是向框架询问其依赖性。

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

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

使用IoC的类似代码:

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

IoC的好处是:

  • 你不依赖于中心 框架,所以这可以改变,如果 期望的。
  • 由于创建了对象 通过注射,优选使用 接口,很容易创建单位 用于替换依赖项的测试 模拟版本。
  • 解密代码。

答案 9 :(得分:1)

依赖倒置原理(DIP)

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

问题:

//A -> B
class A {
  B b

  func foo() {
     b = B();
  }
}

解决方案:

//A -> IB <|- B
//client[A -> IB] <|- B is the Inversion 
class A {
  B ib

  func foo() {
     ib = B()
  }
}

现在A不依赖于B(一对一),现在A依赖于IB实现的接口B,它意味着 A 依赖于 IB(一对多)

的多重实现

[DIP vs DI vs IoC]

答案 10 :(得分:1)

如果我们可以假设某公司的“高级”员工为执行其计划而获得报酬,并且这些计划是通过许多“低级”员工计划的总执行而交付的,则我们可以说,如果高级别员工的计划说明以任何方式与任何低级别员工的具体计划相结合,那通常是一个糟糕的计划。

如果高层管理人员有“改善交货时间”的计划,并指出船运公司的员工每天早上必须喝咖啡并伸懒腰,那么该计划是高度耦合的,凝聚力很低。但是,如果该计划没有提及任何特定员工,而实际上仅要求“一个可以执行工作的实体准备工作”,那么该计划将变得松散耦合且更具凝聚力:这些计划不会重叠,并且很容易被替换。承包商或机器人可以轻松替换员工,高层的计划保持不变。

依赖反转原理中的“高级”表示“更重要”。

答案 11 :(得分:1)

依赖倒置的意义在于制作可重用的软件。

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

最常见的方法是通过像Java中的Spring一样的控制反转(IoC)容器。在这个模型中,对象的属性是通过XML配置而不是出去的对象设置的,并找到它们的依赖关系。

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

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

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

public class MyClass
{
  public IService myService;
}

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

现在,MyClass可以很容易地在其他项目中重用,而不会带来其他两个类的依赖。

更好的是,您不必拖动MyService的依赖项,以及这些依赖项的依赖项,以及......嗯,您明白了。

答案 12 :(得分:0)

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

控制反转:Main与Abstract,以及Main是系统的粘合剂。

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-decoupled-peers/

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

答案 13 :(得分:0)

除了总的来说,好的答案之外,我还想添加自己的一小部分样本,以说明良好与不良做法。是的,我不是一个扔石头的人!

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

class Program
{
    static void Main(string[] args)
    {
        /*
         * BadEncoder: High-level class *contains* low-lever 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接口感到满意; IReadableIWriteable的任何底层实现都不会打扰它。

答案 14 :(得分:0)

假设我们有两个类:EngineerProgrammer

Class Engineer依赖于Programmer类,如下所示:

class Engineer () {

    fun startWork(programmer: Programmer){
        programmer.work()
    }
}

class Programmer {

    fun work(){
        //TODO Do some work here!
    }
}

在此示例中,类Engineer对我们的Programmer类具有依赖性。 如果我需要更改Programmer,该怎么办?

显然,我也需要更改Engineer。 (哇,这时也违反了OCP

然后,我们该如何清理这个烂摊子?答案实际上是抽象。 通过抽象,我们可以删除这两个类之间的依赖关系。例如,我可以为Programmer类创建一个Interface,从现在开始,每个要使用Programmer的类都必须使用其Interface,然后通过更改Programmer类,我们不需要由于我们使用了抽象,因此无需更改使用它的任何类。

注意:DependencyInjection可以帮助我们完成DIPSRP

答案 15 :(得分:0)

Martin Fowler的

Inversion of Control Containers and the Dependency Injection pattern也很好读。我发现Head First Design Patterns是一本很棒的书,因为我第一次尝试学习DI和其他模式。

答案 16 :(得分:-1)

依赖性反转原理(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();
        }
    }

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

答案 17 :(得分:-1)

我可以看到上面的答案给出了很好的解释。但是,我想通过简单的示例提供一些简单的解释。

依赖关系反转原理允许程序员删除硬编码的依赖关系,从而使应用程序变得松散耦合且可扩展。

如何通过抽象实现该目标

无依赖项反转:

 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;
    }
}