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

问题描述 投票:166回答:14

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

oop solid-principles glossary principles dependency-inversion
14个回答
102
投票

查看此文档:The Dependency Inversion Principle

它基本上说:

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

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

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

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


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的依赖项,以及这些依赖项的依赖项,以及......嗯,您明白了。


0
投票

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


0
投票

依赖倒置:取决于抽象,而不是结构。

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

这些是一些讨论这个问题的好帖子:

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/


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(“Clean Architecture”)与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实施都不会打扰它。


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

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


135
投票

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

为了提供摘要,依赖性倒置原则主要是将依赖性的传统方向从“较高级别”组件转换为“较低级别”组件,使得“较低级别”组件依赖于“较高级别”组件所拥有的接口。 。 (注意:这里的“更高级别”组件是指需要外部依赖/服务的组件,不一定是它在分层体系结构中的概念位置。)这样做,耦合不会因为从理论上从组件转移而减少太多对理论上更有价值的组件而言价值较低。

这是通过设计组件来实现的,这些组件的外部依赖性是根据组件的使用者必须提供实现的接口来表示的。换句话说,定义的接口表示组件所需的内容,而不是组件的使用方式(例如“INeedSomething”,而不是“IDoSomething”)。

依赖性倒置原则未提及的是通过使用接口(例如MyService→[ILogger⇐Logger])抽象依赖性的简单实践。虽然这将组件与依赖项的特定实现细节分离,但它不会反转使用者和依赖项之间的关系(例如[MyService→IMyServiceLogger]⇐Logger。

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

在这个重用的一般目标中,我们可以描述两种子类型的重用:

  1. 在具有子依赖项实现的多个应用程序中使用软件组件(例如,您已开发了DI容器并希望提供日志记录,但不希望将容器与特定记录器耦合,以便使用容器的每个人也必须使用您选择的日志库)。
  2. 在不断变化的上下文中使用软件组件(例如,您已经开发了业务逻辑组件,这些组件在实现细节不断发展的应用程序的多个版本中保持不变)。

第一种情况是在多个应用程序之间重用组件,例如使用基础架构库,目标是为消费者提供核心基础结构需求,而不将消费者与自己库的子依赖关系联系起来,因为依赖于这些依赖关系需要消费者也需要相同的依赖关系。当您的库的用户选择使用不同的库来满足相同的基础架构需求时(例如NLog与log4net),或者他们选择使用与版本不向后兼容的所需库的更高版本时,这可能会出现问题您的图书馆需要。

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

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

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

可以在here找到对这个原理的更长时间的讨论,因为它涉及接口的简单使用,依赖注入和分离的接口模式。此外,关于该原理如何与动态类型语言(如JavaScript)相关的讨论可以是foudn here


11
投票

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

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

依赖性倒置原则指出:

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

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


10
投票

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

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

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

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


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

Clean architecture中,域位于中心,如果您指向指示依赖性的箭头方向,则很清楚哪些是最重要且最稳定的层。外层被认为是不稳定的工具,所以要避免依赖它们。

Hexagonal Architecture

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


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. Logic现在可以更容易测试。您现在可以使用伪数据直接实例化Data并将其传入。无需模拟或复杂的测试脚手架。

5
投票

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

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

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

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


4
投票

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

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

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

使用IoC的类似代码:

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

IoC的好处是:

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