已经发布了几个关于依赖注入的特定问题的问题,例如何时使用它以及有哪些框架可供使用。然而,
什么是依赖注入以及何时/为什么应该或不应该使用它?
已经发布了几个关于依赖注入的特定问题的问题,例如何时使用它以及有哪些框架可供使用。然而,
什么是依赖注入以及何时/为什么应该或不应该使用它?
到目前为止,我发现的最好的定义是James Shore的定义:
“依赖注入”是 5 美分概念的 25 美元术语。[...] 依赖注入意味着给一个对象它的实例变量。[...]。
Martin Fowler的一篇文章也可能被证明是有用的。
依赖注入基本上是提供对象需要的对象(它的依赖项),而不是让它自己构造它们。这是一种非常有用的测试技术,因为它允许模拟或删除依赖项。
可以通过多种方式(例如构造函数注入或 setter 注入)将依赖项注入到对象中。甚至可以使用专门的依赖注入框架(例如 Spring)来做到这一点,但它们当然不是必需的。您不需要这些框架来进行依赖注入。显式实例化和传递对象(依赖项)与框架注入一样好。
依赖注入是将依赖传递给其他对象或框架(依赖注入器)。
依赖注入使测试更容易。注入可以通过构造函数完成。
SomeClass()
其构造函数如下:
public SomeClass() {
myObject = Factory.getObject();
}
问题:如果myObject
涉及到磁盘访问或网络访问等复杂任务,则很难在SomeClass()
. 程序员必须模拟myObject
并可能拦截工厂调用。
替代解决方案:
myObject
作为参数传入构造函数public SomeClass (MyClass myObject) {
this.myObject = myObject;
}
myObject
可以直接通过,这使得测试更容易。
在没有依赖注入的情况下,很难在单元测试中隔离组件。
2013 年,当我写这个答案时,这是Google 测试博客上的一个主题。这对我来说仍然是最大的优势,因为程序员在他们的运行时设计中并不总是需要额外的灵活性(例如,对于服务定位器或类似的模式)。程序员经常需要在测试期间隔离类。
我在松散耦合方面发现了这个有趣的例子:
资料来源:了解依赖注入
任何应用程序都由许多对象组成,这些对象相互协作以执行一些有用的事情。传统上,每个对象都负责获取自己对与之协作的依赖对象(依赖项)的引用。这导致了高度耦合的类和难以测试的代码。
例如,考虑一个Car
对象。
ACar
依靠车轮、发动机、燃料、电池等来运行。传统上,我们定义此类依赖对象的品牌以及对象的定义Car
。
没有依赖注入(DI):
class Car{
private Wheel wh = new NepaliRubberWheel();
private Battery bt = new ExcideBattery();
//The rest
}
在这里,Car
对象负责创建依赖对象。
如果我们想Wheel
在初始NepaliRubberWheel()
穿刺之后改变它的依赖对象的类型——比如说——怎么办?我们需要用它的新依赖重新创建 Car 对象 say ChineseRubberWheel()
,但只有Car
制造商可以这样做。
那么这Dependency Injection
对我们有什么作用……?
当使用依赖注入时,对象在运行时而不是编译时(汽车制造时)被赋予它们的依赖关系。这样我们现在就可以随时更改Wheel
。这里,dependency
( wheel
) 可以在运行时注入Car
。
使用依赖注入后:
在这里,我们在运行时注入依赖项(Wheel 和 Battery)。因此,术语:依赖注入。我们通常依靠 Spring、Guice、Weld 等 DI 框架来创建依赖项并在需要的地方注入。
class Car{
private Wheel wh; // Inject an Instance of Wheel (dependency of car) at runtime
private Battery bt; // Inject an Instance of Battery (dependency of car) at runtime
Car(Wheel wh,Battery bt) {
this.wh = wh;
this.bt = bt;
}
//Or we can have setters
void setWheel(Wheel wh) {
this.wh = wh;
}
}
优点是:
依赖注入是一种实践,其中对象的设计方式是从其他代码段接收对象的实例,而不是在内部构造它们。这意味着任何实现对象所需接口的对象都可以在不更改代码的情况下被替换,从而简化了测试,并提高了解耦性。
例如,考虑这些类:
public class PersonService {
public void addManager( Person employee, Person newManager ) { ... }
public void removeManager( Person employee, Person oldManager ) { ... }
public Group getGroupByManager( Person manager ) { ... }
}
public class GroupMembershipService() {
public void addPersonToGroup( Person person, Group group ) { ... }
public void removePersonFromGroup( Person person, Group group ) { ... }
}
在这个例子中,PersonService::addManager
and的实现PersonService::removeManager
需要一个实例GroupMembershipService
来完成它的工作。如果没有依赖注入,传统的做法是GroupMembershipService
在构造函数中实例化一个 newPersonService
并在两个函数中使用该实例属性。但是,如果 的构造函数需要多个东西,或者更GroupMembershipService
糟糕的是,需要在取决于。此外,到的链接被硬编码到,这意味着你不能“假装”GroupMembershipService
PersonService
GroupMembershipService
GroupMembershipService
GroupMembershipService
PersonService
GroupMembershipService
出于测试目的,或在应用程序的不同部分使用策略模式。
使用依赖注入,GroupMembershipService
您PersonService
可以将其传递给PersonService
构造函数,或者添加一个属性(getter 和 setter)来设置它的本地实例,而不是在 . 这意味着您PersonService
不再需要担心如何创建 a GroupMembershipService
,它只接受它给出的那些,并与它们一起工作。这也意味着任何是 的子类GroupMembershipService
或实现GroupMembershipService
接口的东西都可以“注入”到 中PersonService
,并且PersonService
不需要知道更改。
公认的答案是一个很好的答案——但我想补充一点,DI 非常类似于在代码中避免硬编码常量的经典做法。
当您使用一些常量(如数据库名称)时,您会快速将其从代码内部移动到某个配置文件,并将包含该值的变量传递到需要它的地方。这样做的原因是这些常量通常比代码的其余部分更频繁地更改。例如,如果您想在测试数据库中测试代码。
在面向对象编程的世界中,DI 与此类似。那里的值而不是常量文字是整个对象-但是将创建它们的代码从类代码中移出的原因是相似的-对象的更改比使用它们的代码更频繁。需要进行此类更改的一个重要案例是测试。
让我们尝试使用Car和Engine类的简单示例,任何汽车都需要引擎才能去任何地方,至少现在是这样。下面是没有依赖注入的代码的外观。
public class Car
{
public Car()
{
GasEngine engine = new GasEngine();
engine.Start();
}
}
public class GasEngine
{
public void Start()
{
Console.WriteLine("I use gas as my fuel!");
}
}
为了实例化 Car 类,我们将使用下面的代码:
Car car = new Car();
这段代码的问题是我们与 GasEngine 紧密耦合,如果我们决定将其更改为 ElectricityEngine,那么我们将需要重写 Car 类。应用程序越大,我们必须添加和使用新型引擎的问题和头痛就越多。
换句话说,这种方法是我们的高级 Car 类依赖于较低级别的 GasEngine 类,这违反了 SOLID 的依赖倒置原则 (DIP)。DIP 建议我们应该依赖抽象,而不是具体的类。因此,为了满足这一点,我们引入了 IEngine 接口并重写了如下代码:
public interface IEngine
{
void Start();
}
public class GasEngine : IEngine
{
public void Start()
{
Console.WriteLine("I use gas as my fuel!");
}
}
public class ElectricityEngine : IEngine
{
public void Start()
{
Console.WriteLine("I am electrocar");
}
}
public class Car
{
private readonly IEngine _engine;
public Car(IEngine engine)
{
_engine = engine;
}
public void Run()
{
_engine.Start();
}
}
现在我们的 Car 类只依赖于 IEngine 接口,而不是引擎的具体实现。现在,唯一的技巧是我们如何创建 Car 的实例并为其提供一个实际的具体 Engine 类,例如 GasEngine 或 ElectricityEngine。这就是依赖注入的用武之地。
Car gasCar = new Car(new GasEngine());
gasCar.Run();
Car electroCar = new Car(new ElectricityEngine());
electroCar.Run();
在这里,我们基本上将我们的依赖项(Engine 实例)注入(传递)到 Car 构造函数。所以现在我们的类在对象和它们的依赖之间有了松散的耦合,我们可以很容易地添加新类型的引擎而不改变 Car 类。
依赖注入的主要好处是类更松散耦合,因为它们没有硬编码的依赖关系。这遵循上面提到的依赖倒置原则。类不是引用特定的实现,而是请求在构造类时提供给它们的抽象(通常是接口)。
所以说到底,依赖注入只是一种实现对象与其依赖之间松耦合的技术。与其直接实例化类执行其操作所需的依赖项,不如通过构造函数注入(最常见)向类提供依赖项。
此外,当我们有许多依赖项时,使用控制反转(IoC)容器是非常好的做法,我们可以告诉哪些接口应该映射到所有依赖项的哪些具体实现,并且我们可以让它在构造时为我们解决这些依赖项我们的对象。例如,我们可以在 IoC 容器的映射中指定IEngine依赖项应该映射到GasEngine类,当我们向 IoC 容器请求Car类的实例时,它会自动构造带有GasEngine依赖项的Car类通过了。
更新:最近观看了 Julie Lerman 的关于 EF Core 的课程,也喜欢她对 DI 的简短定义。
依赖注入是一种模式,允许您的应用程序动态地将对象注入到需要它们的类中,而无需强制这些类对这些对象负责。它允许您的代码更加松散耦合,并且 Entity Framework Core 插入到同一个服务系统中。
假设你想去钓鱼:
如果没有依赖注入,您需要自己处理所有事情。你需要找一条船,买一根钓鱼竿,寻找诱饵,等等。当然有可能,但这会让你承担很多责任。用软件术语来说,这意味着您必须对所有这些东西进行查找。
通过依赖注入,其他人负责所有准备工作并为您提供所需的设备。您将收到(“被注射”)船、钓鱼竿和诱饵 - 都可以使用。
这是我见过的关于依赖注入和依赖注入容器最简单的解释:
依赖注入和依赖注入容器是不同的东西:
您不需要容器来进行依赖注入。但是,容器可以帮助您。
在进行技术描述之前,首先用一个真实的例子来形象化它,因为你会发现很多技术资料来学习依赖注入,但大多数人无法理解它的核心概念。
在第一张图片中,假设您有一家拥有很多单位的汽车厂。汽车实际上是在装配单元中制造的,但它需要发动机、座椅和车轮。因此,装配单元依赖于这些所有单元,它们是工厂的依赖项。
您会觉得现在维护这个工厂的所有任务太复杂了,因为除了主要任务(在组装单元中组装汽车)您还必须专注于其他单元。现在维护成本非常高,而且厂房很大,所以要多花钱租。
现在,看第二张图。如果您发现一些供应商公司会以比您自己生产成本更低的价格为您提供车轮、座椅和发动机,那么现在您不需要在您的工厂制造它们。您现在可以为您的装配单元租用较小的建筑物,这将减少您的维护任务并降低您的额外租金成本。现在您也可以只专注于您的主要任务(汽车组装)。
现在我们可以说组装汽车的所有依赖项都是从供应商注入到工厂的。这是一个真实的依赖注入(DI)的例子。
现在用技术术语来说,依赖注入是一种技术,一个对象(或静态方法)提供另一个对象的依赖项。因此,将创建对象的任务转移给其他人并直接使用依赖项称为依赖项注入。
“依赖注入”不只是意味着使用参数化构造函数和公共设置器吗?
没有依赖注入的构造函数:
public class Example { private DatabaseThingie myDatabase; public Example() { myDatabase = new DatabaseThingie(); } public void doStuff() { ... myDatabase.getData(); ... } }
具有依赖注入的构造函数:
public class Example { private DatabaseThingie myDatabase; public Example(DatabaseThingie useThisDatabaseInstead) { myDatabase = useThisDatabaseInstead; } public void doStuff() { ... myDatabase.getData(); ... } }
使依赖注入概念易于理解。让我们以开关按钮为例来切换(开/关)灯泡。
Switch 需要事先知道我连接到哪个灯泡(硬编码依赖)。所以,
Switch -> PermanentBulb //switch 直接接永久灯泡,不易测试
Switch(){
PermanentBulb = new Bulb();
PermanentBulb.Toggle();
}
Switch只知道我需要打开/关闭传递给我的任何灯泡。所以,
Switch -> Bulb1 OR Bulb2 OR NightBulb(注入依赖)
Switch(AnyBulb){ //pass it whichever bulb you like
AnyBulb.Toggle();
}
修改开关和灯泡的James示例:
public class SwitchTest {
TestToggleBulb() {
MockBulb mockbulb = new MockBulb();
// MockBulb is a subclass of Bulb, so we can
// "inject" it here:
Switch switch = new Switch(mockBulb);
switch.ToggleBulb();
mockBulb.AssertToggleWasCalled();
}
}
public class Switch {
private Bulb myBulb;
public Switch() {
myBulb = new Bulb();
}
public Switch(Bulb useThisBulbInstead) {
myBulb = useThisBulbInstead;
}
public void ToggleBulb() {
...
myBulb.Toggle();
...
}
}`
什么是依赖注入(DI)?
正如其他人所说,依赖注入(DI)消除了我们感兴趣的类(消费者类)所依赖的其他对象实例(在UML 意义上)的直接创建和生命周期管理的责任。这些实例被传递给我们的消费者类,通常作为构造函数参数或通过属性设置器(依赖对象实例化和传递给消费者类的管理通常由控制反转(IoC)容器执行,但这是另一个主题) .
DI、DIP 和 SOLID
具体来说,在 Robert C Martin 的面向对象设计的 SOLID 原则范式中,是依赖倒置原则 (DIP)DI
的可能实现之一。DIP 是口头禅- 其他 DIP 实现包括服务定位器和插件模式。D
SOLID
DIP 的目标是解耦类之间紧密的、具体的依赖关系,而是通过抽象来放松耦合,这可以通过或来实现interface
,具体取决于所使用的语言和方法。abstract class
pure virtual class
如果没有 DIP,我们的代码(我称之为“消费类”)直接耦合到一个具体的依赖项,并且通常还承担着知道如何获取和管理此依赖项实例的责任,即概念上:
"I need to create/use a Foo and invoke method `GetBar()`"
而在应用 DIP 之后,要求放宽了,并且消除了获取和管理Foo
依赖项生命周期的问题:
"I need to invoke something which offers `GetBar()`"
为什么使用 DIP(和 DI)?
以这种方式解耦类之间的依赖关系可以很容易地用其他实现替换这些依赖类,这些实现也满足抽象的先决条件(例如,可以用相同接口的另一个实现切换依赖关系)。此外,正如其他人所提到的,通过 DIP 解耦类的最常见原因可能是允许单独测试消费类,因为现在可以对这些相同的依赖项进行存根和/或模拟。
DI 的一个后果是依赖对象实例的生命周期管理不再由消费类控制,因为依赖对象现在被传递到消费类(通过构造函数或 setter 注入)。
这可以通过不同的方式查看:
Create
根据需要通过工厂上的 a 获取实例,并在完成后处置这些实例。何时使用 DI?
MyDepClass
线程安全吗?如果我们将其设为单例并将相同的实例注入所有消费者会怎样?)例子
这是一个简单的 C# 实现。鉴于以下消费类:
public class MyLogger
{
public void LogRecord(string somethingToLog)
{
Console.WriteLine("{0:HH:mm:ss} - {1}", DateTime.Now, somethingToLog);
}
}
虽然看似无害,但它对另外两个类有两个依赖static
,System.DateTime
和System.Console
一个非确定性的系统时钟。
然而,我们可以应用DIP
到这个类,将时间戳作为依赖项抽象出来,并且MyLogger
只耦合到一个简单的接口:
public interface IClock
{
DateTime Now { get; }
}
我们还可以放松对抽象的依赖Console
,例如TextWriter
. 依赖注入通常被实现为constructor
注入(将抽象传递给依赖作为参数给消费类的构造函数)或Setter Injection
(通过setXyz()
setter 或定义的 .Net 属性传递依赖{set;}
)。构造函数注入是首选,因为这样可以保证类在构造后处于正确状态,并允许将内部依赖字段标记为readonly
(C#) 或final
(Java)。所以在上面的例子中使用构造函数注入,这给我们留下了:
public class MyLogger : ILogger // Others will depend on our logger.
{
private readonly TextWriter _output;
private readonly IClock _clock;
// Dependencies are injected through the constructor
public MyLogger(TextWriter stream, IClock clock)
{
_output = stream;
_clock = clock;
}
public void LogRecord(string somethingToLog)
{
// We can now use our dependencies through the abstraction
// and without knowledge of the lifespans of the dependencies
_output.Write("{0:yyyy-MM-dd HH:mm:ss} - {1}", _clock.Now, somethingToLog);
}
}
(Clock
需要提供一个具体的,当然可以恢复为DateTime.Now
,并且这两个依赖项需要由 IoC 容器通过构造函数注入提供)
可以构建一个自动化的单元测试,它明确地证明我们的记录器工作正常,因为我们现在可以控制依赖关系 - 时间,我们可以监视书面输出:
[Test]
public void LoggingMustRecordAllInformationAndStampTheTime()
{
// Arrange
var mockClock = new Mock<IClock>();
mockClock.Setup(c => c.Now).Returns(new DateTime(2015, 4, 11, 12, 31, 45));
var fakeConsole = new StringWriter();
// Act
new MyLogger(fakeConsole, mockClock.Object)
.LogRecord("Foo");
// Assert
Assert.AreEqual("2015-04-11 12:31:45 - Foo", fakeConsole.ToString());
}
下一步
依赖注入总是与控制反转容器(IoC)相关联,以注入(提供)具体的依赖实例,并管理生命周期实例。在配置/引导过程中,IoC
容器允许定义以下内容:
IBar
,返回一个ConcreteBar
实例”)IDisposable
的责任。Disposing
通常,一旦 IoC 容器被配置/引导,它们就会在后台无缝运行,从而使编码人员可以专注于手头的代码,而不必担心依赖关系。
DI 友好代码的关键是避免类的静态耦合,并且不使用 new() 来创建依赖项
根据上面的示例,依赖关系的解耦确实需要一些设计工作,并且对于开发人员来说,需要进行范式转变来打破new
直接依赖依赖的习惯,而是信任容器来管理依赖关系。
但是好处很多,特别是在彻底测试您感兴趣的课程的能力方面。
注意new ..()
: POCO / POJO / 序列化 DTO / 实体图 / 匿名 JSON 投影等的创建/映射/投影(通过) - 即“仅数据”类或记录 - 使用或从方法返回不被视为依赖项(在UML 意义)并且不受 DI 约束。用来new
投影这些就好了。
以上所有答案都很好,我的目的是用简单的方式解释这个概念,以便任何没有编程知识的人也能理解这个概念
依赖注入是帮助我们以更简单的方式创建复杂系统的设计模式之一。
我们可以在日常生活中看到这种模式的广泛应用。一些例子是磁带录音机、VCD、CD 驱动器等。
上图是 20 世纪中叶卷对卷便携式录音机的图像。来源。
录音机的主要目的是记录或播放声音。
在设计系统时,它需要一个卷轴来记录或播放声音或音乐。设计这个系统有两种可能性
如果我们使用第一个,我们需要打开机器来更换卷轴。如果我们选择第二个,即为卷轴放置一个钩子,我们将获得通过更换卷轴播放任何音乐的额外好处。并且还减少了仅在卷轴中播放任何内容的功能。
同样,依赖注入是将依赖关系外部化以仅关注组件的特定功能的过程,以便可以将独立的组件耦合在一起以形成复杂的系统。
我们通过使用依赖注入获得的主要好处。
现在,这些概念构成了编程世界中众所周知的框架的基础。Spring Angular 等是建立在这个概念之上的知名软件框架
依赖注入是一种用于创建其他对象所依赖的对象实例的模式,而在编译时不知道哪个类将用于提供该功能,或者只是将属性注入对象的方式称为依赖注入。
依赖注入示例
以前我们是这样写代码的
Public MyClass{
DependentClass dependentObject
/*
At somewhere in our code we need to instantiate
the object with new operator inorder to use it or perform some method.
*/
dependentObject= new DependentClass();
dependentObject.someMethod();
}
使用依赖注入,依赖注入器将为我们取消实例化
Public MyClass{
/* Dependency injector will instantiate object*/
DependentClass dependentObject
/*
At somewhere in our code we perform some method.
The process of instantiation will be handled by the dependency injector
*/
dependentObject.someMethod();
}
你也可以阅读
依赖注入 (DI) 的全部意义在于保持应用程序源代码的干净和稳定:
实际上,每个设计模式都会将关注点分开,以使未来的更改影响最小的文件。
DI 的具体领域是依赖配置和初始化的委托。
如果您偶尔在 Java 之外工作,请回想一下source
在许多脚本语言(Shell、Tcl 等,甚至import
在 Python 中被滥用于此目的)中经常使用的方式。
考虑简单的dependent.sh
脚本:
#!/bin/sh
# Dependent
touch "one.txt" "two.txt"
archive_files "one.txt" "two.txt"
该脚本是依赖的:它不会自行成功执行(archive_files
未定义)。
您archive_files
在archive_files_zip.sh
实现脚本中定义(zip
在这种情况下使用):
#!/bin/sh
# Dependency
function archive_files {
zip files.zip "$@"
}
不是source
直接在依赖项中 -ing 实现脚本,而是使用injector.sh
包装两个“组件”的“容器”:
#!/bin/sh
# Injector
source ./archive_files_zip.sh
source ./dependent.sh
archive_files
依赖项刚刚被注入到依赖脚本中。
您可以注入archive_files
使用tar
或实现的依赖项xz
。
如果dependent.sh
脚本直接使用依赖,则该方法称为依赖查找(与依赖注入相反):
#!/bin/sh
# Dependent
# dependency look-up
source ./archive_files_zip.sh
touch "one.txt" "two.txt"
archive_files "one.txt" "two.txt"
现在的问题是依赖的“组件”必须自己执行初始化。
“组件”的源代码既不干净也不稳定,因为依赖项初始化的每次更改都需要“组件”的源代码文件的新版本。
DI 不像在 Java 框架中那样被广泛强调和普及。
但这是一种解决以下问题的通用方法:
仅将配置与依赖项查找一起使用并没有帮助,因为配置参数的数量可能会根据依赖项(例如新的身份验证类型)以及受支持的依赖项类型的数量(例如新的数据库类型)而改变。
例如,我们有 2 个班级Client
和Service
. Client
将使用Service
public class Service {
public void doSomeThingInService() {
// ...
}
}
方式1)
public class Client {
public void doSomeThingInClient() {
Service service = new Service();
service.doSomeThingInService();
}
}
方式2)
public class Client {
Service service = new Service();
public void doSomeThingInClient() {
service.doSomeThingInService();
}
}
方式3)
public class Client {
Service service;
public Client() {
service = new Service();
}
public void doSomeThingInClient() {
service.doSomeThingInService();
}
}
1) 2) 3) 使用
Client client = new Client();
client.doSomeThingInService();
好处
缺点
Client
课难Service
构造函数时,我们需要在所有地方更改代码创建Service
对象方式一)构造函数注入
public class Client {
Service service;
Client(Service service) {
this.service = service;
}
// Example Client has 2 dependency
// Client(Service service, IDatabas database) {
// this.service = service;
// this.database = database;
// }
public void doSomeThingInClient() {
service.doSomeThingInService();
}
}
使用
Client client = new Client(new Service());
// Client client = new Client(new Service(), new SqliteDatabase());
client.doSomeThingInClient();
方式2) Setter注入
public class Client {
Service service;
public void setService(Service service) {
this.service = service;
}
public void doSomeThingInClient() {
service.doSomeThingInService();
}
}
使用
Client client = new Client();
client.setService(new Service());
client.doSomeThingInClient();
方式3)接口注入
检查https://en.wikipedia.org/wiki/Dependency_injection
===
现在,这段代码已经被遵循,测试类Dependency Injection
更容易了。
但是,我们仍然使用了很多时间,并且在更改构造函数时并不好。为了防止它,我们可以使用 DI 注射器,如
1) 简单的手动Client
new Service()
Service
Injector
public class Injector {
public static Service provideService(){
return new Service();
}
public static IDatabase provideDatatBase(){
return new SqliteDatabase();
}
public static ObjectA provideObjectA(){
return new ObjectA(provideService(...));
}
}
使用
Service service = Injector.provideService();
2) 使用库:Android dagger2
好处
Service
,只需要在 Injector 类中更改即可Constructor Injection
,当你查看构造函数时,你会看到类Client
的依赖项有多少Client
缺点
Constructor Injection
,Service
对象是在创建时Client
创建的,有时我们在Client
类中使用函数而不使用Service
所以创建Service
是浪费的https://en.wikipedia.org/wiki/Dependency_injection
依赖项是可以使用的对象(
Service
)
注入是将依赖项()传递给将使用它Service
的依赖对象( )Client
依赖注入(DI)意味着解耦相互依赖的对象。假设对象 A 依赖于对象 B,因此想法是将这些对象彼此分离。尽管有编译时间,我们不需要使用 new 关键字对对象进行硬编码,而是在运行时共享对对象的依赖关系。如果我们谈论
我们不需要使用 new 关键字对对象进行硬编码,而是在配置文件中定义 bean 依赖项。弹簧容器将负责连接所有。
IOC 是一个通用概念,它可以用多种不同的方式表达,而依赖注入是 IOC 的一个具体例子。
当容器调用具有多个参数的类构造函数时,就完成了基于构造函数的 DI,每个参数都表示对其他类的依赖。
public class Triangle {
private String type;
public String getType(){
return type;
}
public Triangle(String type){ //constructor injection
this.type=type;
}
}
<bean id=triangle" class ="com.test.dependencyInjection.Triangle">
<constructor-arg value="20"/>
</bean>
基于 Setter 的 DI 是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化 bean 后调用 bean 上的 setter 方法来完成的。
public class Triangle{
private String type;
public String getType(){
return type;
}
public void setType(String type){ //setter injection
this.type = type;
}
}
<!-- setter injection -->
<bean id="triangle" class="com.test.dependencyInjection.Triangle">
<property name="type" value="equivialteral"/>
注意:对于强制依赖项使用构造函数参数和对可选依赖项使用设置器是一个很好的经验法则。请注意,如果我们在 setter 上使用基于注释而不是 @Required 注释可用于使 setter 成为必需的依赖项。
我能想到的最好的类比是手术室里的外科医生和他的助手,外科医生是主要人员,他的助手在需要时提供各种手术组件,以便外科医生可以专注于一个他最擅长的事情(手术)。如果没有助手,外科医生每次需要时都必须自己获取组件。
简而言之,DI 是一种通过向组件提供组件来消除组件上常见的额外责任(负担)以获取依赖组件的技术。
DI 让您更接近单一职责 (SR) 原则,例如surgeon who can concentrate on surgery
.
何时使用 DI:我建议在几乎所有生产项目(小型/大型)中使用 DI,尤其是在不断变化的业务环境中:)
原因:因为您希望您的代码易于测试、可模拟等,以便您可以快速测试您的更改并将其推向市场。此外,当您有很多很棒的免费工具/框架来支持您在您拥有更多控制权的代码库的过程中,您为什么不这样做。
这意味着对象应该只具有完成其工作所需的依赖项,并且依赖项应该很少。此外,如果可能,对象的依赖关系应该在接口上,而不是在“具体”对象上。(具体对象是使用关键字 new 创建的任何对象。)松散耦合促进了更高的可重用性、更容易的可维护性,并允许您轻松地提供“模拟”对象来代替昂贵的服务。
“依赖注入”(DI)也称为“控制反转”(IoC),可用作鼓励这种松散耦合的技术。
实施 DI 有两种主要方法:
这是将对象依赖项传递给其构造函数的技术。
请注意,构造函数接受接口而不是具体对象。另请注意,如果 orderDao 参数为空,则会引发异常。这强调了接收有效依赖项的重要性。在我看来,构造函数注入是赋予对象依赖关系的首选机制。开发人员在调用对象时很清楚需要将哪些依赖项提供给“Person”对象才能正确执行。
但是请考虑以下示例……假设您有一个包含十个没有依赖关系的方法的类,但是您正在添加一个确实依赖于 IDAO 的新方法。您可以更改构造函数以使用构造函数注入,但这可能会迫使您更改所有构造函数调用。或者,您可以只添加一个接受依赖项的新构造函数,但是开发人员如何轻松知道何时使用一个构造函数而不是另一个。最后,如果依赖项的创建成本很高,那么在它可能很少使用的情况下,为什么还要创建它并传递给构造函数呢?“Setter Injection”是另一种可用于此类情况的 DI 技术。
Setter 注入不会强制将依赖项传递给构造函数。相反,依赖关系被设置到需要的对象公开的公共属性上。如前所述,这样做的主要动机包括:
以下是上述代码的示例:
public class Person {
public Person() {}
public IDAO Address {
set { addressdao = value; }
get {
if (addressdao == null)
throw new MemberAccessException("addressdao" +
" has not been initialized");
return addressdao;
}
}
public Address GetAddress() {
// ... code that uses the addressdao object
// to fetch address details from the datasource ...
}
// Should not be called directly;
// use the public property instead
private IDAO addressdao;
我知道已经有很多答案,但我发现这很有帮助:http ://tutorials.jenkov.com/dependency-injection/index.html
public class MyDao {
protected DataSource dataSource = new DataSourceImpl(
"driver", "url", "user", "password");
//data access methods...
public Person readPerson(int primaryKey) {...}
}
public class MyDao {
protected DataSource dataSource = null;
public MyDao(String driver, String url, String user, String password) {
this.dataSource = new DataSourceImpl(driver, url, user, password);
}
//data access methods...
public Person readPerson(int primaryKey) {...}
}
注意DataSourceImpl
实例化是如何移动到构造函数中的。构造函数接受四个参数,它们是DataSourceImpl
. 尽管MyDao
该类仍然依赖于这四个值,但它本身不再满足这些依赖关系。它们由创建MyDao
实例的任何类提供。
我想既然每个人都为DI写过,让我问几个问题..
这是基于@Adam N 发布的答案。
为什么 PersonService 再也不用担心 GroupMembershipService 了?您刚刚提到 GroupMembership 有多个它依赖的东西(对象/属性)。如果 PService 中需要 GMService,那么您可以将其作为属性。无论您是否注入它,您都可以模拟它。我唯一希望注入它的是 GMService 是否有更具体的子类,直到运行时你才会知道。然后你想注入子类。或者,如果您想将其用作单例或原型。老实说,配置文件对编译时要注入的类型(接口)的子类进行了硬编码。
编辑
DI 通过消除确定依赖方向和编写任何胶水代码的任何需要来增加凝聚力。
错误的。依赖的方向是XML形式或注解,你的依赖被写成XML代码和注解。XML 和注释是源代码。
DI 通过使所有组件模块化(即可替换)并具有明确定义的相互接口来减少耦合。
错误的。您不需要 DI 框架来构建基于接口的模块化代码。
关于可替换:使用非常简单的 .properties 存档和 Class.forName 您可以定义哪些类可以更改。如果您的代码的任何类都可以更改,Java 不适合您,请使用脚本语言。顺便说一句:不重新编译就无法更改注释。
在我看来,DI 框架的唯一原因是:减少样板。使用完善的工厂系统,您可以像首选的 DI 框架那样做同样的事情,更可控和更可预测,DI 框架承诺代码减少(XML 和注释也是源代码)。问题是这种样板减少在非常非常简单的情况下是真实的(每个类一个实例和类似的),有时在现实世界中选择适当的服务对象并不像将一个类映射到一个单例对象那么容易。
流行的答案是无用的,因为它们以一种无用的方式定义依赖注入。让我们同意,“依赖”是指我们的对象 X 需要的一些预先存在的其他对象。但是当我们说时我们并没有说我们正在做“依赖注入”
$foo = Foo->new($bar);
我们只是将其称为将参数传递给构造函数。自从构造函数被发明以来,我们就经常这样做。
“依赖注入”被认为是一种“控制反转”,这意味着从调用者中取出一些逻辑。当调用者传入参数时,情况并非如此,所以如果那是 DI,DI 不会暗示控制反转。
DI 意味着在调用者和管理依赖的构造函数之间有一个中间层。Makefile 是依赖注入的一个简单示例。“调用者”是在命令行中键入“make bar”的人,“构造者”是编译器。Makefile 指定 bar 依赖于 foo,它执行
gcc -c foo.cpp; gcc -c bar.cpp
在做之前
gcc foo.o bar.o -o bar
键入“make bar”的人不需要知道 bar 依赖于 foo。在“make bar”和 gcc 之间注入了依赖项。
中间层的主要目的不仅仅是将依赖项传递给构造函数,而是将所有依赖项都列出在一个地方,并对编码器隐藏它们(而不是让编码器提供它们)。
通常中间层为构造的对象提供工厂,它必须提供每个请求的对象类型必须满足的角色。那是因为有了一个隐藏构造细节的中间层,你已经招致了工厂强加的抽象惩罚,所以你还不如使用工厂。
依赖注入意味着一种方式(实际上是任何方式)代码的一部分(例如一个类)以模块化的方式访问依赖项(代码的其他部分,例如它所依赖的其他类),而无需对其进行硬编码(因此它们可以根据需要自由更改或覆盖,甚至可以在其他时间加载)
(和 ps ,是的,它已成为一个相当简单的概念的过度炒作的 25 美元名称),我的.25
美分
依赖注入是通常被称为“依赖混淆”要求的一种可能的解决方案。依赖混淆是一种从向需要它的类提供依赖的过程中去除“明显”性质的方法,因此以某种方式混淆了向所述类提供所述依赖。这不一定是坏事。事实上,通过混淆向类提供依赖项的方式,然后类外部的东西负责创建依赖项,这意味着在各种情况下,可以将依赖项的不同实现提供给类而无需进行任何更改到班级。这非常适合在生产模式和测试模式之间切换(例如,使用“模拟”服务依赖项)。
不幸的是,有些人认为您需要一个专门的框架来进行依赖混淆,并且如果您选择不使用特定的框架来做这件事,那么您在某种程度上就是一个“小”程序员。许多人认为,另一个非常令人不安的神话是依赖注入是实现依赖混淆的唯一方法。从历史上看,这显然是 100% 错误的,但是您将难以说服某些人有依赖注入的替代方法来满足您的依赖混淆要求。
多年来,程序员已经了解依赖混淆的要求,并且在依赖注入被构想之前和之后,许多替代解决方案已经发展起来。有工厂模式,但也有许多使用 ThreadLocal 的选项,其中不需要注入到特定实例 - 依赖项被有效地注入到线程中,这有利于使对象可用(通过方便的静态 getter 方法)任何需要它的类,而不必向需要它的类添加注释并设置复杂的 XML“胶水”来实现它。当您的依赖项需要持久性(JPA/JDO 或其他)时,它允许您更轻松地实现“透明持久性”,并使用纯粹由 POJO 组成的域模型和业务模型类(即没有特定于框架的/锁定在注释中)。
摘自《扎实的 Java 开发人员:Java 7 和多语言编程的重要技术》一书
DI 是 IoC 的一种特殊形式,因此查找依赖项的过程不受当前执行代码的直接控制。
5岁儿童的依赖注入。
当你自己去冰箱里取东西时,你可能会出问题。你可能会把门开着,你可能会得到一些妈妈或爸爸不希望你拥有的东西。您甚至可能正在寻找我们甚至没有或已过期的东西。
你应该做的是陈述一个需求,“我午餐需要喝点东西”,然后我们会确保你坐下来吃饭时有东西。
来自Book Apress.Spring.Persistence.with.Hibernate.Oct.2010
依赖注入的目的是将解析外部软件组件的工作与应用程序业务逻辑分离。没有依赖注入,组件如何访问所需服务的细节可能会与组件的代码混淆。这不仅增加了错误的可能性,增加了代码膨胀,并放大了维护的复杂性;它将组件更紧密地耦合在一起,使得在重构或测试时很难修改依赖关系。
依赖注入 (DI) 来自设计模式,它使用 OOP 的基本特征——一个对象与另一个对象的关系。虽然继承继承一个对象来执行更复杂和更具体的另一个对象,但关系或关联只是使用属性从一个对象创建指向另一个对象的指针。DI 的强大功能与 OOP 的其他特性相结合,如接口和隐藏代码。假设我们在图书馆有一个客户(订户),为了简单起见,他只能借一本书。
书籍界面:
package com.deepam.hidden;
public interface BookInterface {
public BookInterface setHeight(int height);
public BookInterface setPages(int pages);
public int getHeight();
public int getPages();
public String toString();
}
接下来我们可以拥有多种书籍;一种类型是虚构的:
package com.deepam.hidden;
public class FictionBook implements BookInterface {
int height = 0; // height in cm
int pages = 0; // number of pages
/** constructor */
public FictionBook() {
// TODO Auto-generated constructor stub
}
@Override
public FictionBook setHeight(int height) {
this.height = height;
return this;
}
@Override
public FictionBook setPages(int pages) {
this.pages = pages;
return this;
}
@Override
public int getHeight() {
// TODO Auto-generated method stub
return height;
}
@Override
public int getPages() {
// TODO Auto-generated method stub
return pages;
}
@Override
public String toString(){
return ("height: " + height + ", " + "pages: " + pages);
}
}
现在订阅者可以关联到这本书:
package com.deepam.hidden;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class Subscriber {
BookInterface book;
/** constructor*/
public Subscriber() {
// TODO Auto-generated constructor stub
}
// injection I
public void setBook(BookInterface book) {
this.book = book;
}
// injection II
public BookInterface setBook(String bookName) {
try {
Class<?> cl = Class.forName(bookName);
Constructor<?> constructor = cl.getConstructor(); // use it for parameters in constructor
BookInterface book = (BookInterface) constructor.newInstance();
//book = (BookInterface) Class.forName(bookName).newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return book;
}
public BookInterface getBook() {
return book;
}
public static void main(String[] args) {
}
}
所有这三个类都可以隐藏起来以实现它自己的实现。现在我们可以将此代码用于 DI:
package com.deepam.implement;
import com.deepam.hidden.Subscriber;
import com.deepam.hidden.FictionBook;
public class CallHiddenImplBook {
public CallHiddenImplBook() {
// TODO Auto-generated constructor stub
}
public void doIt() {
Subscriber ab = new Subscriber();
// injection I
FictionBook bookI = new FictionBook();
bookI.setHeight(30); // cm
bookI.setPages(250);
ab.setBook(bookI); // inject
System.out.println("injection I " + ab.getBook().toString());
// injection II
FictionBook bookII = ((FictionBook) ab.setBook("com.deepam.hidden.FictionBook")).setHeight(5).setPages(108); // inject and set
System.out.println("injection II " + ab.getBook().toString());
}
public static void main(String[] args) {
CallHiddenImplBook kh = new CallHiddenImplBook();
kh.doIt();
}
}
如何使用依赖注入有很多不同的方法。可以和Singleton等结合,但基本上还是只是通过在另一个对象内部创建对象类型的属性来实现关联。有用性仅在功能上,我们应该一次又一次编写的代码总是为我们准备和完成。这就是为什么 DI 与控制反转 (IoC) 紧密结合的原因,这意味着我们的程序将控制权传递给另一个正在运行的模块,该模块将 bean 注入我们的代码。(每个可以注入的对象都可以被签名或视为一个 Bean。)例如在 Spring 中,它是通过创建和初始化ApplicationContext来完成的容器,它为我们工作。我们只需在代码中创建上下文并调用 bean 的初始化。在那一刻,注射已自动完成。
依赖注入 (DI) 是依赖倒置原则 (DIP) 实践的一部分,也称为控制反转 (IoC)。基本上你需要做 DIP 因为你想让你的代码更加模块化和单元测试,而不仅仅是一个单一的系统。因此,您开始识别可以与类分离并抽象出来的代码部分。现在需要从类外部注入抽象的实现。通常这可以通过构造函数来完成。因此,您创建了一个接受抽象作为参数的构造函数,这称为依赖注入(通过构造函数)。有关 DIP、DI 和 IoC 容器的更多说明,您可以阅读此处
来自 Christoffer Noring,Pablo Deeleman 的书“Learning Angular - Second Edition”:
“随着我们的应用程序的增长和发展,我们的每个代码实体都将在内部需要其他对象的实例,这些对象在软件工程领域中被更好地称为依赖项。将此类依赖项传递给依赖客户端的操作称为注入,它还需要另一个代码实体的参与,称为注入器。注入器将负责实例化和引导所需的依赖项因此,从成功注入客户端的那一刻起,它们就可以使用了。这一点非常重要,因为客户端对如何实例化自己的依赖项一无所知,只知道它们为了使用它们而实现的接口。”
来自:安东·莫伊谢耶夫。《使用 Typescript 进行 Angular 开发,第二版》一书:
“简而言之,DI可以帮助您以松耦合的方式编写代码,并使您的代码更具可测试性和可重用性。”</p>
我会提出一个稍微不同的、简短而精确的定义,即依赖注入是什么,关注主要目标,而不是技术手段(从这里开始):
依赖注入是创建服务对象的静态、无状态图的过程,其中每个服务都通过其依赖项进行参数化。
我们在应用程序中创建的对象(无论我们使用 Java、C# 还是其他面向对象的语言)通常属于以下两类之一:无状态、静态和全局“服务对象”(模块),以及有状态、动态和本地“数据对象”。
模块图——服务对象的图——通常在应用程序启动时创建。这可以使用容器来完成,例如 Spring,但也可以通过将参数传递给对象构造函数来手动完成。两种方式各有利弊,但在您的应用程序中使用 DI 绝对不需要框架。
一项要求是服务必须通过其依赖项进行参数化。这意味着什么完全取决于给定系统中采用的语言和方法。通常,这采用构造函数参数的形式,但使用 setter 也是一种选择。这也意味着服务的依赖项对服务的用户是隐藏的(在调用服务方法时)。
什么时候使用?我会说,只要应用程序足够大,将逻辑封装到单独的模块中,模块之间的依赖图就会提高代码的可读性和可探索性。
简而言之,依赖注入 (DI) 是消除不同对象之间的依赖关系或紧密耦合的方法。依赖注入为每个对象提供了一个内聚的行为。
DI 是 Spring 的 IOC 主体的实现,它说“不要打电话给我们,我们会打电话给你”。使用依赖注入程序员不需要使用 new 关键字创建对象。
对象一旦加载到 Spring 容器中,然后我们在需要时通过使用 getBean(String beanName) 方法从 Spring 容器中获取这些对象来重用它们。
依赖注入是与 Spring 框架相关概念的核心。在创建任何项目的框架时,spring 可能会发挥至关重要的作用,而依赖注入在这里出现了。
实际上,假设您在java中创建了两个不同的类,分别是A类和B类,并且您想在A类中使用B类中可用的任何功能,那么此时可以使用依赖注入。您可以在另一个类中创建一个类的对象,就像您可以在另一个类中注入整个类以使其可访问一样。通过这种方式可以克服依赖。
依赖注入只是简单地粘合两个类,同时保持它们分开。
依赖注入是基于框架构建的“控制反转”原则的一种实现。
GoF的“设计模式”中所述的框架是实现主要控制流逻辑的类,促使开发人员这样做,通过这种方式框架实现了控制原则的反转。
一种作为技术实现的方法,而不是作为类层次结构,这个 IoC 原则就是依赖注入。
DI主要包括将类实例和类型引用的映射委托给外部“实体”:对象、静态类、组件、框架等......
类实例是“依赖”,调用组件通过引用与类实例的外部绑定就是“注入”。
显然,您可以从 OOP 的角度以多种方式实现此技术,例如构造函数注入、setter 注入、接口注入。
委托第三方执行将 ref 匹配到对象的任务,当您想要将需要某些服务的组件与相同的服务实现完全分离时,这非常有用。
这样,在设计组件时,您可以专注于它们的架构和它们的特定逻辑,信任与其他对象协作的接口,而不用担心使用的对象/服务的任何类型的实现更改,即使您正在使用相同的对象将被完全替换(显然尊重接口)。
依赖注入是使解耦组件与它们的一些依赖无关的做法,这遵循SOLID指南,即
依赖倒置原则:应该“依赖于抽象,而不是具体。
依赖注入的更好实现是组合根设计模式,因为它允许您的组件与依赖注入容器分离。
我推荐这篇关于 Composition Root 的精彩文章 http://blog.ploeh.dk/2011/07/28/CompositionRoot/ 由 Mark Seemann 撰写
以下是这篇文章的要点:
组合根是模块组合在一起的应用程序中的(最好)唯一位置。
...
只有应用程序应该有组合根。库和框架不应该。
...
DI Container 只能从 Composition Root 引用。所有其他模块都不应引用容器。
Di-Ninja 的文档,一个依赖注入框架,是一个很好的例子来展示组合根和依赖注入的原理是如何工作的。 https://github.com/di-ninja/di-ninja 据我所知,它是 JavaScript 中唯一一个实现 Composition-Root 设计模式的 DiC。
我们可以实现一个依赖注入来了解它:
class Injector {
constructor() {
this.dependencies = {};
this.register = (key, value) => {
this.dependencies[key] = value;
};
}
resolve(...args) {
let func = null;
let deps = null;
let scope = null;
const self = this;
if (typeof args[0] === 'string') {
func = args[1];
deps = args[0].replace(/ /g, '').split(',');
scope = args[2] || {};
} else {
func = args[0];
deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(',');
scope = args[1] || {};
}
return (...args) => {
func.apply(scope || {}, deps.map(dep => self.dependencies[dep] && dep != '' ? self.dependencies[dep] : args.shift()));
}
}
}
injector = new Injector();
injector.register('module1', () => { console.log('hello') });
injector.register('module2', () => { console.log('world') });
var doSomething1 = injector.resolve(function (module1, module2, other) {
module1();
module2();
console.log(other);
});
doSomething1("Other");
console.log('--------')
var doSomething2 = injector.resolve('module1,module2,', function (a, b, c) {
a();
b();
console.log(c);
});
doSomething2("Other");
以上是javascript的实现
任何重要的应用程序都由两个或多个相互协作以执行某些业务逻辑的类组成。传统上,每个对象都负责获取自己对与之协作的对象(其依赖项)的引用。应用 DI 时,对象在创建时由协调系统中每个对象的某个外部实体赋予它们的依赖关系。换句话说,依赖项被注入到对象中。
有关更多详细信息,请参阅在此处输入链接描述
DI 是真实对象之间实际交互的方式,而不需要一个对象负责另一个对象的存在。对象应该被平等对待。它们都是对象。没有人应该表现得像一个创造者。这就是你如何公正对待你的对象。
简单的例子:
如果您需要医生,您只需去找(现有的)医生。您不会考虑从头开始创建一名医生来帮助您。他已经存在,他可能会为你或其他对象服务。无论您(单个对象)是否需要他,他都有权存在,因为他的目的是为一个或多个对象服务。决定他存在的是全能神,而不是自然选择。因此,DI 的一个优点是避免在你的 Universe(即应用程序)的生命周期中创建无用的冗余对象。
在Dependency Injection (DI)
中,组件的依赖关系由名为 的外部实体作为输入提供injector
。注入器使用提供者创建依赖项。提供者是知道如何创建依赖项的配方。
注入器初始化不同的组件并将它们的依赖关系联系在一起。它可以是一个简单的初始化脚本,也可以是一个更复杂的全局容器,映射所有依赖关系并集中系统所有模块的布线。这种方法的主要优点是改进了解耦,尤其是对于依赖于有状态实例的模块(例如,数据库连接)。使用 DI,从外部接收每个依赖项,而不是硬编码到模块中。这意味着依赖模块可以配置为使用任何兼容的依赖项,因此模块本身可以在不同的上下文中重用。依赖注入增加了应用程序的灵活性和模块化。在应用程序中使用它可以使代码灵活、可测试和可变。