it-swarm.cn

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

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

164
Phillip Wells

检查此文档: 依赖性倒置原则

它基本上说:

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

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

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

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

102
Carl Seleborg

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

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

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

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

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

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

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

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

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

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

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

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

关于这个原理的更长时间的讨论,因为它涉及接口的简单使用,依赖注入和分离的接口模式可以找到 这里

133
Derek Greer

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

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

依赖性倒置原则指出:

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

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

11
nikhil.singhal

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

传统的分层架构

传统上,分层架构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

深化原则

重要的是要充分理解这一概念,深化目的和利益。如果我们机械地停留并学习典型的案例库,我们将无法确定我们可以应用依赖原则的位置。

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

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

更改持久性类型更容易,数据库或技术访问与域逻辑相同的数据库或用于与持久性通信的操作。因此,依赖性是相反的,因为如果发生这种变化,更容易改变持久性。通过这种方式,我们不必更改域名。域层是最稳定的,这就是为什么它不应该依赖于任何东西。

但是,不仅有这个存储库示例。在许多场景中,该原则适用,并且存在基于该原理的架构。

架构

存在依赖性反转是其定义的关键的体系结构。在所有域中,它是最重要的,它是抽象,表明域和其他包或库之间的通信协议已定义。

清洁架构

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

六角形建筑

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

9
xurxodev

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

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

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

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

9
Rogério

基本上它说:

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

8
martin.ra

陈述依赖性倒置原则的更明确的方法是:

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

即,不像人们通常那样实现你的类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
mattvonb

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

原因 _ dip _ 很重要,因为它确保了OO原理“松耦合设计”。

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

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

5
Hace

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

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

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

使用IoC的类似代码:

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

IoC的好处是:

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

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

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

最常见的方法是通过像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的依赖项,以及这些依赖项的依赖项,以及......嗯,您明白了。

1
Marc Hughes

我想我有更好(更直观)的例子。

  • 想象一下带有员工和联系人管理的系统(webapp)(两个屏幕)。
  • 它们并不完全相关,因此您希望它们各自位于自己的模块/文件夹中

所以你会有一些“主要”入口点,它必须知道员工管理模块联系人管理模块,它必须在导航中提供链接并接受api请求等。换句话说,主要模块将取决于这两个 - 它将知道他们的控制器,路线和必须在(共享)导航中呈现的链接。

Node.js示例

// main.js
import express from 'express'

// two modules, each having many exports
import { api as contactsApi, navigation as cNav } from './contacts/'
import { api as employeesApi, navigation as eNav } from './employees/'

const api = express()
const navigation = {
  ...cNav,
  ...eNav
}

api.use('contacts', contactsApi)
api.use('employees', employeesApi)

// do something with navigation, possibly do some other setup

此外,请注意 有些情况(简单的),这是完全正常的。


因此,随着时间的推移,添加新模块并不是那么简单。你必须记住注册api,导航,也许 权限 ,这个main.js变得越来越大。

这就是依赖倒置的地方。您将引入一些“核心”并使每个模块自身注册,而不是您的主模块依赖于所有其他模块。

所以在这种情况下,它是关于一些ApplicationModule的概念,它可以将自己提交给许多服务(路由,导航,权限),主模块可以保持简单(只需导入模块并让它安装)

换句话说,它是关于制作可插拔架构。这是额外的工作和代码,你必须编写/阅读和维护,所以你不应该提前做,而是当你有这种气味。

特别有趣的是你可以创建一个插件,甚至是持久层 - 如果你需要支持许多持久性实现,这可能是值得做的,但通常情况并非如此。查看具有六边形体系结构的图像的其他答案,它非常适用于插图 - 有一个核心,其他一切基本上都是一个插件。

0
Kamil Tomšík

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

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

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/

0

控制容器的反转和依赖注入模式 by Martin Fowler也是一本很好的读物。我发现 He​​ad First Design Patterns 是一本很棒的书,是我第一次尝试学习DI和其他模式。

0
Chris Canal

除了其他答案....

让我先举一个例子......

有一家酒店要求食品生产商提供食品。酒店将食物的名称(比如鸡肉)提供给食物发生器,发电机将要求的食物送回酒店。但酒店并不关心它所接收和服务的食物类型。因此,发电机向酒店提供标有“食物”的食物。

这个实现是用Java编写的

带有工厂方法的FactoryClass。食物发生器

public class FoodGenerator {
    Food food;
    public Food getFood(String name){
        if(name.equals("fish")){
            food =  new Fish();
        }else if(name.equals("chicken")){
            food =  new Chicken();
        }else food = null;

        return food;
    }
}


抽象/接口类

public abstract class Food {

    //None of the child class will override this method to ensure quality...
    public void quality(){
        String fresh = "This is a fresh " + getName();
        String tasty = "This is a tasty " + getName();
        System.out.println(fresh);
        System.out.println(tasty);
    }
    public abstract String getName();
}


鸡肉食品(混凝土类)

public class Chicken extends Food {
    /*All the food types are required to be fresh and tasty so
     * They won't be overriding the super class method "property()"*/

    public String getName(){
        return "Chicken";
    }
}


鱼类实施食物(混凝土类)

public class Fish extends Food {
    /*All the food types are required to be fresh and tasty so
     * They won't be overriding the super class method "property()"*/

    public String getName(){
        return "Fish";
    }
}


最后

酒店

public class Hotel {

    public static void main(String args[]){
        //Using a Factory class....
        FoodGenerator foodGenerator = new FoodGenerator();
        //A factory method to instantiate the foods...
        Food food = foodGenerator.getFood("chicken");
        food.quality();
    }
}

正如您所看到的,酒店不知道它是鸡肉对象还是鱼类对象。它只知道它是一个食物对象,即酒店取决于食物类别。

您还会注意到鱼类和鸡类实施的食品类别与酒店没有直接关系。即鸡肉和鱼类也取决于食品类别。

这意味着高级组件(酒店)和低级组件(鱼和鸡)都取决于抽象(食物)。

这称为依赖性倒置。

0
Revolver

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

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

0
Rejwanul Reja