依赖注入与依赖倒置的区别

133

有两种设计模式,即“依赖注入”和“依赖倒置”。网络上已经有文章试图解释它们之间的区别。但还需要用更简单的话来解释。有没有人可以帮忙呢?

我需要用 PHP 来理解它。


不是PHP,但请查看我的答案:https://dev59.com/Omcs5IYBdhLWcg3wHwfU#13109003 - David Osborne
1
如果你要问一个重复的问题,至少你应该链接你已经阅读过的文章和帖子,并列出你不理解的细节。 - jaco0646
@jaco0646,我说的是那些不在SO上的文章。 - Istiaque Ahmed
5个回答

227
(注意:这个答案是与语言无关的,尽管问题特别提到了PHP,但由于不熟悉PHP,我没有提供任何PHP示例)。
术语 - 依赖和耦合
在面向对象编程的上下文中,一个“依赖”是指类与其他“对象类型”之间存在直接关系。当一个类直接依赖于另一种对象类型时,可以说它与该类型“耦合”。
一般来说,类使用的任何类型都在某种程度上是一个依赖。类依赖于另一种类型的方式有很多种,包括:
- 实例变量使用的对象类型 - 构造函数参数使用的对象类型 - 访问器/修改器方法使用的对象类型 - 直接创建新对象的构造函数(有时也是方法) - 继承
类与其依赖之间的关系越紧密,耦合度就越高;因此,当一个类直接依赖于另一个具体类(例如通过继承在基类上创建直接依赖,或者构造函数为其实例变量创建新对象的情况),对该直接依赖的任何未来更改更有可能以蝴蝶效应的方式"波及"整个系统。
注射与倒装的区别
  • 依赖注入是一种通过控制反转技术,通过依赖注入设计模式向类提供对象('依赖项')的方法。通常通过以下方式传递依赖项:
    • 构造函数
    • 公共属性或字段
    • 公共设值器
  • 依赖倒置原则(DIP)是软件设计指南,其核心建议是关于将类与具体依赖项解耦合
    1. '高层模块不应该依赖低层模块。两者都应该依赖于抽象。'
    2. '抽象不应该依赖于细节。细节应该依赖于抽象。'
或者,更简洁地说:
- 依赖注入是一种用于填充类的实例变量的实现技术。 - 依赖倒置是一个通用的设计准则,建议类只与高层抽象有直接关系。

依赖注入和控制反转(IoC)

依赖注入通过确保类不负责创建或提供自己的依赖项(因此也不负责这些依赖项的生命周期),来应用IoC原则。

然而,控制反转并不等同于依赖注入 - 实际上,作为一种原则,控制反转与依赖项或依赖注入本身没有特别的关系;依赖注入是基于控制反转原则的设计模式。

控制反转在许多其他上下文中都有出现,包括与对象创建或依赖项无关的完全不相关的情况,例如通过中介者或消息泵传递消息以触发事件处理程序。其他(无关的)控制反转示例包括:

  • 使用事件处理函数/方法处理鼠标/键盘输入事件的窗口化应用程序。
  • 使用控制器动作处理HTTP请求的MVC Web应用程序。

(从原始答案更新为关于控制反转的单独解释)


依赖注入模式

依赖注入是一种设计模式,它应用了控制反转(IoC)原则,以确保一个类在构造函数或实例变量中使用的对象的创建和生命周期与其完全无关或不知情 -- 对象的创建和填充实例变量的“常见”问题被推迟到框架中处理。

也就是说,一个类可以指定其实例变量,但不会为填充这些实例变量做任何工作(除非使用构造函数参数作为“传递”)。

一个考虑到依赖注入的设计的类可能如下所示:

// Dependency Injection Example...

class Foo {
    // Constructor uses DI to obtain the Meow and Woof dependencies
    constructor(fred: Meow, barney: Woof) {
        this.fred = fred;
        this.barney = barney;
    }
}

在这个例子中,MeowWoof都是通过Foo构造函数进行注入的依赖项。
另一方面,一个没有使用依赖注入设计的Foo类可能会简单地自己创建MeowWoof实例,或者使用某种服务定位器/工厂。
// Example without Dependency Injection...

class Foo {
    constructor() {
        // a 'Meow' instance is created within the Foo constructor
        this.fred = new Meow();

        // a service locator gets a 'WoofFactory' which in-turn
        // is responsible for creating a 'Woof' instance.
        // This demonstrates IoC but not Dependency Injection.
        var factory = TheServiceLocator.GetWoofFactory();
        this.barney = factory.CreateWoof();
    }
}

依赖注入简单来说就是一个类推迟了获取或提供自己依赖的责任,而将这个责任交给想要创建实例的对象。通常情况下,这个对象就是一个控制反转容器(IoC Container)。

依赖倒置原则(DIP)

依赖倒置主要是通过防止具体类之间有直接引用来解耦。

DIP的主要关注点是确保一个类只依赖于更高级别的抽象。例如,接口存在于比具体类更高级别的抽象层次上。

DIP并不是关于注入依赖,尽管依赖注入模式是许多技术中的一种,可以帮助提供所需的间接性,避免依赖低级细节和与其他具体类的耦合。

注意:在静态类型的编程语言(如C#或Java)中,依赖倒置通常更加明确,因为这些语言对变量名进行严格的类型检查。另一方面,在动态语言(如Python或JavaScript)中,依赖倒置已经被动地可用,因为这些语言中的变量没有特定的类型限制。

考虑一个在静态类型语言中的场景,其中一个类需要能够从应用程序的数据库中读取记录:

// class Foo depends upon a concrete class called SqlRecordReader.

class Foo {
    reader: SqlRecordReader;

    constructor(sqlReader: SqlRecordReader) {
        this.reader = sqlReader;
    }

    doSomething() {
        var records = this.reader.readAll();
        // etc.
    }
}

在上面的例子中,尽管使用了依赖注入,但是类Foo仍然对SqlRecordReader有一个硬性依赖,然而它真正关心的只是存在一个名为readAll()的方法,该方法返回一些记录。
考虑到将SQL数据库查询重构为独立的微服务的情况,这需要对代码库进行更改;Foo类需要从远程服务中读取记录。或者,当Foo单元测试需要从内存存储或平面文件中读取数据时的情况。
如果像其名称所暗示的那样,SqlRecordReader包含数据库和SQL逻辑,那么任何转向微服务的举措都需要更改Foo类。
依赖反转原则建议用一个更高级的抽象来替换SqlRecordReader,该抽象仅提供readAll()方法。即:
interface IRecordReader {
    Records[] getAll();
}

class Foo {
    reader: IRecordReader;

    constructor(reader: IRecordReader) {
        this.reader = reader;
    }
}

根据DIP原则,IRecordReaderSqlRecordReader是一个更高级的抽象。将Foo从依赖于SqlRecordReader改为依赖于IRecordReader可以满足DIP准则。
为什么DIP准则很有用
关键词是"准则" - 依赖倒置在程序设计中增加了间接性。增加任何形式的间接性的明显缺点是复杂性增加(即人类理解所需的认知负荷增加)。
在许多情况下,间接性可以使代码更易于维护(修复错误,添加增强功能),但是:
在最后一个示例中,Foo可能会接收一个SqlRecordReader,或者可能是一个SoapRecordReader,或者可能是一个FileRecordReader,甚至为了单元测试可能是一个MockRecordReader - 关键是它对于IRecordReader的不同可能实现一无所知,也不关心 - 当然,前提是这些实现符合Liskov替换原则
此外,它避免了潜在的混乱场景,其中一个匆忙想要让某个东西工作的开发人员可能考虑通过从基类 `SqlRecordReader` 继承 `SoapRecordReader` 或 `FileRecordReader` 来“瞒骗”Liskov原则。
更糟糕的是,一个经验不足的开发人员甚至可能改变 `SqlRecordReader` 本身,以便该类不仅具有 SQL 的逻辑,还具有 SOAP 端点、文件系统和任何其他可能需要的东西的逻辑。(这种情况在现实世界中经常发生-特别是在维护不良的代码中,并且几乎总是一种代码异味。)

11
依赖反转和控制反转(IoC)是相同的吗? - Istiaque Ahmed
3
与其相关但不同的是:依赖反转 是指“细节取决于抽象”(即解耦,使得一个类不依赖于其他具体的类),而 控制反转 (IoC) 是关于能够使用某种通用框架来创建/连接对象并提供依赖。IoC 也可以是通过某种通用接口实际控制对象的使用通用框架(例如,一堆不同的类,每个类都实现自己的 Run() 方法,该方法可以被通用框架调用)。 - Ben Cottrell
这可能会为IoC提供更好的上下文:https://en.wikipedia.org/wiki/Inversion_of_control - Ben Cottrell
我在想,既然 IRecordReader 已经是一个抽象类,因此解决了直接依赖于实现细节的问题,为什么仍然建议从外部传递 IRecordReader?那么使用 Foo 的类不就依赖于 IRecordReader 了吗?感觉 IRecordReader 的依赖只是被移动了。 - Linus
1
@ahnbizcad 感谢您的评论;简单回答一下,它们有些正交,DIP 是指在代码中表达类关系的抽象级别(即“大局”设计/结构;例如,依赖于接口而不是具体类)。另一方面,依赖注入将对象之间的“连线”转变为运行时决策,而不是编译时决策嵌入到该类的代码中 - 即它成为流程控制的问题,而不是代码结构的问题。我已经做了一些修改,以使这更清晰。 - Ben Cottrell
显示剩余3条评论

29

请查看此文 这里

作者用简单的语言区分了这两个概念。依赖注入 == “给我它”,而依赖倒置 == “有人以某种方式替我处理这个”。在依赖倒置原则中,高层模块是抽象的所有者。因此,细节(抽象的实现)取决于抽象,因此也取决于高级模块。依赖反转!.. 依赖注入不同。抽象可能不由高级模块保留。因此,提供给更高级对象的抽象可能不仅限于高级模块的需求。

依赖倒置:

您有一个高层模块 X 和一个由 X 定义的抽象 Y。Z 实现了 Y,并被赋予 X。因此,Z 依赖于 X(通过 X 定义的抽象 Y)。

依赖注入:

您有一个需要功能 A 和 B 的高层模块 X。Y 是包含功能 A、B 和 C 的抽象。Z 实现了 Y。由于 Z 实现了 Y,因此具有功能 A 和 B,将 Z 给予 X。现在 X 依赖于 Y。


2
之前看过这个。你能在这里用更简单的话概括一下吗? - Istiaque Ahmed
如果您正在寻找答案,请查看更新后的答案。 - AJA

2
"依赖注入"是对象提供其他对象依赖项的能力。简单地说,这意味着某些东西依赖于其他东西。例如,A类使用B类的几个函数,现在A类需要创建B类的实例,这里就可以使用DI。
"控制反转(IOC)"是为了反转不同的职责,例如你需要在家工作,但你需要烹饪才能吃饭,现在你可以在网上订餐,它会送到你家门口,这意味着你可以专注于工作。这里你把烹饪的责任反转给了在线餐厅。
依赖倒置原则(DIP)指出,高级模块不应该依赖于低级模块;两者都应该依赖于抽象。抽象不应该依赖于细节,而细节应该依赖于抽象。"

1

依赖注入是实现控制反转的一种方式(我假设您所指的依赖反转是指控制反转),因此两者并不是在竞争,而 DI 是 IoC 的一种专业化。其他常见的实现 IoC 的方法包括使用工厂或服务定位器模式。


依赖反转并不等同于控制反转,对吗? - Istiaque Ahmed
5
依赖倒置原则(Dependency inversion)与依赖注入(dependency injection)或控制反转(inversion of control)无关。它是SOLID原则中的“D”。请参考其他答案获取更深入的见解,并通过谷歌搜索SOLID来了解更多信息。 - makoshichi

0
依赖倒置:你如何设计你的类和接口(高层次应该依赖于抽象,而不是低层次的细节)。
public interface ILog
{
    void WriteLog(string message);
}

public class Log : ILog
{
    public void WriteLog(string message)
    {
        // Implementation details...
    }
}

public class Test
{
    private readonly ILog _obj;

    public Test(ILog obj)
    {
        _obj = obj;
    }

    // Other members...
}

依赖注入:一种机制,用于向类提供或“注入”这些依赖项(可以手动或使用框架实现)。 在某些依赖注入设置代码中,比如在ASP.NET Core中的Program.cs中实现依赖注入的示例。
builder.Services.AddTransient<ILog, Log>();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接