存在两种设计模式,即依赖注入和依赖倒置,网上有文章试图解释差异。但是仍然需要用更简单的语言来解释它。有没有人愿意来?
我需要用 PHP 来理解它。
存在两种设计模式,即依赖注入和依赖倒置,网上有文章试图解释差异。但是仍然需要用更简单的语言来解释它。有没有人愿意来?
我需要用 PHP 来理解它。
(注意:这个答案与语言无关,虽然问题特别提到了 PHP,但由于不熟悉 PHP,我没有提供任何 PHP 示例)。
在面向对象编程的上下文中,依赖是与类有直接关系的任何其他对象类型。当一个类直接依赖于另一个对象类型时,它可以被描述为与该类型耦合。
一般来说,类使用的任何类型在某种程度上都是依赖关系。一个类依赖于另一种类型有很多不同的方式,包括:
一个类与其依赖关系越强,耦合越紧密;因此,当一个类直接依赖于另一个具体类时(例如,继承创建对基类的直接依赖的情况,或者构造函数为其实例变量创建新对象的情况),该直接依赖的任何未来更改都将更可能以蝴蝶效应的方式“涟漪”。
依赖注入是一种控制反转技术,用于通过依赖注入设计模式向类提供对象(“依赖”) 。通常通过以下方式之一传递依赖项:
依赖倒置原则 (DIP) 是一个软件设计指南,它归结为关于将类与其具体依赖关系解耦的两条建议:
或者,更简洁地说:
依赖注入通过确保类从不负责创建或提供它们自己的依赖项(因此也不对这些依赖项的生命周期负责)来应用 IoC 原则。
然而,IoC 不是依赖注入——事实上,作为一个原则的 IoC 与依赖或依赖注入本身并没有什么特别的关系;依赖注入是一种基于 IoC 原理的设计模式。
IoC 出现在许多其他上下文中,包括与对象创建或依赖关系完全无关的上下文,例如通过中介或消息泵传递消息以触发事件处理程序。IoC 的其他(不相关)示例包括:
(从原始答案更新为关于 IoC 的单独解释)
依赖注入是一种设计模式,它应用 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;
}
}
在本例中,Meow
和Woof
都是通过构造函数注入的依赖项。Foo
另一方面,一个没有Foo
依赖注入设计的类可能只是简单地创建和实例本身,或者可能使用某种服务定位器/工厂:Meow
Woof
// 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 容器)
依赖倒置广泛地是关于通过防止这些类之间有任何直接引用来解耦具体类。
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()
方法的更高级别的抽象来替换。IE:
interface IRecordReader {
Records[] getAll();
}
class Foo {
reader: IRecordReader;
constructor(reader: IRecordReader) {
this.reader = reader;
}
}
根据 DIP,它IRecordReader
是比Foo IRecordReader SqlRecordReader更高级别的抽象,满足 DIP 准则。SqlRecordReader, and forcing
to depend on
instead of
关键字是指南- 依赖倒置为您的程序设计增加了间接性。添加任何间接方式的明显缺点是复杂性(即人类理解正在发生的事情所需的认知“负载”)增加。
在许多情况下,间接可以使代码更易于维护(修复错误,添加增强功能)但是:
在最后一个示例中,Foo
可能会收到 a SqlRecordReader
,或者可能是 a SoapRecordReader
,或者可能是 a FileRecordReader
,或者甚至可能用于单元测试 a MockRecordReader
- 关键是它不知道或不关心任何不同的可能实现IRecordReader
- 当然,如果这些实现实现了到里氏替换原则。
此外,它避免了潜在的肮脏场景,即急于使某些东西工作的开发人员可能会考虑尝试通过从基类继承SoapRecordReader
or来“捏造” Liskov 原则。FileRecordReader
SqlRecordReader
更糟糕的是,没有经验的开发人员甚至可能会更改SqlRecordReader
自身,以便类不仅具有 SQL 的逻辑,而且还具有 SOAP 端点、文件系统和其他任何可能需要的逻辑。(这种事情在现实世界中经常发生——尤其是在维护不善的代码中,而且几乎总是代码异味。)
依赖注入是对象提供其他对象的依赖的能力。简而言之,它意味着其他东西依赖于其他东西。示例 A 类使用了 B 类的几个函数,现在 A 类需要创建 B 类的实例,这里用到了 DI。
国际奥委会将颠倒不同的职责,例如,您需要在家工作,但您需要做饭才能吃饭,现在列出的在家做饭,您可以在线订购,并且可以在家门口使用,这意味着您可以专注于工作。在这里,您将烹饪职责转换为在线恢复。
依赖倒置原则(DIP)指出高层模块不应该依赖于低层模块;两者都应该依赖于抽象。抽象不应该依赖于细节。细节应该取决于抽象。
依赖注入是实现控制反转的一种方法(我假设您将其称为依赖反转),因此两者并没有真正竞争,因为 DI 是 IoC 的专业化。其他常见的实现 IoC 的方法包括使用工厂或服务定位器模式。