已经发布了几个关于依赖注入的具体问题的问题,例如何时使用它以及它有哪些框架.然而,
什么是依赖注入以及何时/为什么应该或不应该使用它?
到目前为止,我发现的最佳定义是James Shore:
对于5美分的概念,"依赖注入"是一个25美元的术语.[...]依赖注入意味着为对象提供其实例变量.[...].
目前由Martin Fowler的文章,可能证明是有用的,太.
依赖注入基本上是提供对象所需的对象(它的依赖关系),而不是让它自己构造它们.这是一种非常有用的测试技术,因为它允许模拟或删除依赖项.
可以通过多种方式(例如构造函数注入或setter注入)将依赖项注入到对象中.甚至可以使用专门的依赖注入框架(例如Spring)来做到这一点,但它们当然不是必需的.您不需要这些框架具有依赖注入.显式地实例化和传递对象(依赖关系)与框架注入一样好.
基本上,不是让对象创建依赖项或要求工厂对象为它们创建一个依赖项,而是将所需的依赖项传递给外部对象,并使其成为别人的问题.这个"某人"要么是依赖图上的对象,要么是构建依赖图的依赖注入器(框架).我在这里使用它的依赖是当前对象需要持有引用的任何其他对象.
依赖注入的一个主要优点是它可以使测试更容易.假设您有一个对象,在其构造函数中执行以下操作:
public SomeClass() {
myObject = Factory.getObject();
}
当你想要做的就是运行一些单元测试时SomeClass()
,这可能很麻烦,特别是如果myObject
是复杂的磁盘或网络访问.所以现在你正在看嘲笑,SomeClass()
但也以某种方式拦截工厂电话.硬.而是将对象作为参数传递给构造函数.现在你已将问题转移到其他地方,但测试可以变得更容易.只需制作一个假人myObject
并将其传入.构造函数现在看起来有点像:
public SomeClass (MyClass myObject) {
this.myObject = myObject;
}
这是依赖注入的一种风格 - 通过构造函数.有几种机制是可能的.
正如评论中所指出的,一种常见的替代方法是定义一个do-nothing构造函数,并通过属性setter(h/t @MikeVella)注入依赖项.
Martin Fowler记录了第三种替代方法(h/t @MarcDix),其中类显式实现了它们希望注入的依赖项的接口.
当不使用依赖注入时(例如在构造函数中执行过多工作的类等),在单元测试中隔离组件往往变得更加困难.
早在2013年,当我写这个答案时,这是Google测试博客上的一个主题.这对我来说仍然是最大的优势,因为您可能并不总是需要在运行时设计中具有额外的灵活性(例如,对于服务定位器或类似的模式),但是您通常需要能够在测试期间隔离您的类.
我发现松耦合这个有趣的例子:
任何应用程序都由许多对象组成,这些对象相互协作以执行一些有用的东西.传统上,每个对象都负责获取它自己对它协作的依赖对象(依赖项)的引用.这导致高度耦合的类和难以测试的代码.
例如,考虑一个Car
对象.
A Car
取决于车轮,发动机,燃料,电池等运行.传统上,我们定义此类依赖对象的品牌以及对象的定义Car
.
没有依赖注入(DI):
class Car{
private Wheel wh = new NepaliRubberWheel();
private Battery bt = new ExcideBattery();
//The rest
}
这里,Car
对象负责创建依赖对象.
如果我们想要改变其依赖对象的类型 - 比如说Wheel
- 在最初的NepaliRubberWheel()
穿孔后怎么办?我们需要重新创建具有新依赖性的Car对象ChineseRubberWheel()
,但只有Car
制造商可以这样做.
那么Dependency Injection
我们做什么......?
使用依赖项注入时,会在运行时为对象提供依赖项,而不是编译时间(汽车制造时间).这样我们现在可以随时改变Wheel
.这里,dependency
(wheel
)可以Car
在运行时注入.
使用依赖注入后:
在这里,我们注入的依赖性在运行时(轮和电池).因此术语:依赖注入.
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
并且PersonService::removeManager
需要一个实例GroupMembershipService
才能完成它的工作.如果没有依赖注入,传统的方法是GroupMembershipService
在构造函数中实例化一个new ,PersonService
并在两个函数中使用该实例属性.但是,如果构造函数GroupMembershipService
有多个需要的东西,或者更糟糕的是,有一些初始化的"setter"需要调用GroupMembershipService
,代码增长相当快,PersonService
现在不仅取决于GroupMembershipService
而且还取决于其他所有东西.GroupMembershipService
依赖于取决于.此外,链接到GroupMembershipService
硬编码,PersonService
这意味着你不能"假装"aGroupMembershipService
用于测试目的,或在应用程序的不同部分使用策略模式.
使用依赖注入,而不是实例化GroupMembershipService
你的内部PersonService
,你要么将它传递给PersonService
构造函数,要么添加一个Property(getter和setter)来设置它的本地实例.这意味着你PersonService
不再需要担心如何创建一个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.这就是Dependency Injection的用武之地.
Car gasCar = new Car(new GasEngine());
gasCar.Run();
Car electroCar = new Car(new ElectricityEngine());
electroCar.Run();
这里我们基本上将我们的依赖项(引擎实例)注入(传递)给Car构造函数.所以现在我们的类在对象及其依赖项之间存在松耦合,我们可以轻松添加新类型的引擎而无需更改Car类.
依赖注入的主要好处是类更松散地耦合,因为它们没有硬编码的依赖关系.这遵循上面提到的依赖性倒置原则.类不是引用特定的实现,而是请求在构造类时提供给它们的抽象(通常是接口).
因此,最终依赖注入只是一种实现对象及其依赖关系之间松散耦合的技术.不是直接实例化类所需的依赖关系以执行其操作,而是通过构造函数注入向类(最常见)提供依赖关系.
此外,当我们有很多依赖关系时,使用Inversion of Control(IoC)容器是非常好的做法,我们可以告诉哪些接口应该映射到我们所有依赖项的哪些具体实现,并且我们可以让它在构造时为我们解析这些依赖关系我们的目标.例如,我们可以在IoC容器的映射中指定IEngine依赖项应该映射到GasEngine类,当我们向IoC容器询问Car类的实例时,它将自动构造具有GasEngine依赖关系的Car类.传入
更新:最近观看了Julie Lerman关于EF Core的课程,也喜欢她关于DI的简短定义.
依赖注入是一种模式,允许应用程序动态地将对象注入需要它们的类,而不必强制这些类负责这些对象.它允许您的代码更松散地耦合,并且Entity Framework Core插入到同一个服务系统中.
让我们想象你想钓鱼:
如果没有依赖注入,您需要自己处理所有事情.你需要找一条船,买一根钓竿,寻找诱饵等等.当然,这是可能的,但它给你带来了很多责任.在软件术语中,这意味着您必须对所有这些事情执行查找.
通过依赖注入,其他人负责所有准备工作并为您提供所需的设备.您将收到("注入")船,钓竿和诱饵 - 所有这些都可以使用.
这是我见过的关于依赖注入和依赖注入容器的最简单的解释:
没有依赖注入
应用程序需要Foo(例如控制器),因此:
应用程序创建了Foo
应用程序调用Foo
Foo需要Bar(例如服务),所以:
Foo创造了Bar
Foo打电话给Bar
Bar需要Bim(服务,存储库......),所以:
酒吧创造了Bim
酒吧做点什么
使用依赖注入
应用程序需要Foo,需要Bar,需要Bim,所以:
应用程序创建Bim
应用程序创建Bar并将其赋予Bim
应用程序创建Foo并为其提供Bar
应用程序调用Foo
Foo打电话给Bar
酒吧做点什么
使用依赖注入容器
应用程序需要Foo所以:
应用程序从Container获取Foo,因此:
容器创造了Bim
Container创建Bar并将其赋予Bim
容器创建Foo并给它Bar
应用程序调用Foo
Foo打电话给Bar
酒吧做点什么
依赖注入和依赖注入容器是不同的东西:
依赖注入是一种编写更好代码的方法
DI容器是一种帮助注入依赖关系的工具
您不需要容器来执行依赖注入.但是容器可以帮助您.
"依赖注入"不仅仅意味着使用参数化构造函数和公共setter吗?
James Shore的文章展示了以下用于比较的例子.
没有依赖注入的构造函数:
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();
...
}
}
使依赖注入概念易于理解.我们举一个开关按钮来切换(打开/关闭)灯泡.
开关需要事先知道我连接的灯泡(硬编码依赖).所以,
开关 - > PermanentBulb //开关直接连接到永久灯泡,测试不容易
Switch(){ PermanentBulb = new Bulb(); PermanentBulb.Toggle(); }
Switch只知道我需要打开/关闭哪个Bulb传递给我.所以,
开关 - > Bulb1或Bulb2 OR NightBulb(注入依赖)
Switch(AnyBulb){ //pass it whichever bulb you like AnyBulb.Toggle(); }
修改开关和灯泡的詹姆斯示例:
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原则的范例中,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
根据需要通过工厂获取实例,并在完成后处理这些实例.
或者,依赖关系实例的生命周期控制可以放弃到IoC容器(更多关于此内容).
何时使用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
抽象的依赖,比如a TextWriter
.依赖注入通常实现为constructor
注入(将依赖项的抽象作为参数传递给消费类的构造函数)或Setter Injection
(通过setXyz()
setter 传递依赖项或使用已{set;}
定义的.Net属性).构造函数注入是首选,因为这可以保证类在构造之后处于正确的状态,并允许将内部依赖项字段标记为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(); 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
实例")
可以为每个依赖项的生命周期管理设置策略,例如为每个使用者实例创建新对象,在所有使用者之间共享单个依赖项实例,仅在同一个线程上共享相同的依赖项实例,等等.
在.Net中,IoC容器知道诸如此类的协议IDisposable
,并将Disposing
根据配置的生命周期管理承担依赖关系的责任.
通常,一旦配置/引导IoC容器,它们就会在后台无缝运行,允许编码器专注于手头的代码,而不是担心依赖性.
DI友好代码的关键是避免类的静态耦合,而不是使用new()来创建依赖项
按照上面的例子,依赖关系的解耦确实需要一些设计工作,对于开发人员来说,需要一种范式转换来直接打破new
依赖关系的习惯,而是信任容器来管理依赖关系.
但好处很多,特别是在彻底测试你感兴趣的课程的能力.
注意:new ..()
POCO/POJO /序列化DTO /实体图/匿名JSON投影等的创建/映射/投影(通过) - 即"仅数据"类或记录 - 从方法中使用或返回不被视为依赖性(在UML意义)并且不受DI的影响.利用new
项目,这些仅仅是罚款.
依赖注入(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
直接在依赖的实现脚本中使用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框架那样受到重视和普及.
但这是分解以下问题的一般方法:
应用程序开发(单一源代码发布生命周期)
应用程序部署(具有独立生命周期的多个目标环境)
仅使用配置依赖项查找没有帮助,因为每个依赖项(例如,新的身份验证类型)以及受支持的依赖项类型(例如,新数据库类型)的配置参数的数量可能会发生变化.
所有上述答案都很好,我的目的是以简单的方式解释这个概念,这样没有编程知识的人也可以理解概念
依赖注入是帮助我们以更简单的方式创建复杂系统的设计模式之一.
我们可以在日常生活中看到这种模式的广泛应用.一些例子是录音机,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)意味着解耦彼此依赖的对象.假设对象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; } }
基于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是一个很好的经验法则.请注意,如果我们在setter上使用基于@Required注释的注释,则可以使用setter作为必需的依赖项.
我能想到的最好的比喻是手术室里的外科医生和他的助手,外科医生是主要人员,他的助手在需要时提供各种手术部件,以便外科医生可以专注于他做得最好的事(手术).没有助手,外科医生每次需要时都必须自己拿到组件.
简而言之,DI是一种通过向组件提供依赖组件来消除组件的常见附加责任(负担)的技术.
DI让您更接近单一责任(SR)原则,就像surgeon who can concentrate on surgery
.
何时使用DI:我建议在几乎所有生产项目(小/大)中使用DI,特别是在不断变化的商业环境中:)
原因:因为您希望您的代码易于测试,可模拟等,以便您可以快速测试您的更改并将其推向市场.除此之外,为什么你不会有很多很棒的免费工具/框架来支持你在你拥有更多控制权的代码库之旅.
例如,我们有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
对象
方法1)构造函数注入
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)塞特犬注射
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
,测试Client
类更容易.
但是,我们仍然使用new Service()
很多时间,并且在更改Service
构造函数时并不好.为了防止它,我们可以使用DI喷射器,如
1)简单的手动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类中更改它
如果你使用use Constructor Injection
,当你看构造函数时Client
,你会看到Client
class有多少依赖
缺点
如果使用use Constructor Injection
,Service
则在Client
创建对象时创建对象,有时我们在Client
没有使用的情况下使用类中的函数,Service
因此Service
浪费了
https://en.wikipedia.org/wiki/Dependency_injection
依赖项是可以使用的对象(
Service
)
注入是将dependency(Service
)传递给Client
将使用它的依赖对象()
这意味着对象应该只具有执行其工作所需的依赖项,并且依赖项应该很少.此外,对象的依赖关系应该在接口上,而不是在"具体"对象上,如果可能的话.(具体对象是使用关键字new创建的任何对象.)松散耦合可提高可重用性,更易于维护,并允许您轻松提供"模拟"对象来代替昂贵的服务.
"依赖注入"(DI)也称为"控制反转"(IoC),可用作鼓励这种松散耦合的技术.
实施DI有两种主要方法:
构造函数注入
塞特犬注射
这是将对象依赖项传递给其构造函数的技术.
请注意,构造函数接受接口而不是具体对象.另请注意,如果orderDao参数为null,则抛出异常.这强调了接收有效依赖的重要性.在我看来,构造函数注入是赋予对象依赖性的首选机制.开发人员在调用对象时很清楚需要将哪些依赖关系赋予"Person"对象以便正确执行.
但请考虑以下示例...假设您有一个具有十个没有依赖关系的方法的类,但您添加的新方法确实依赖于IDAO.您可以更改构造函数以使用构造函数注入,但这可能会强制您更改所有构造函数调用.或者,您可以添加一个新的构造函数来获取依赖项,但是开发人员如何轻松地知道何时使用一个构造函数而不是另一个构造函数.最后,如果创建依赖项非常昂贵,为什么它应该只在很少使用的情况下创建并传递给构造函数?"Setter Injection"是另一种DI技术,可用于此类情况.
Setter Injection不会强制将依赖项传递给构造函数.相反,依赖项设置为有需要的对象公开的公共属性.如前所述,这样做的主要动机包括:
支持依赖注入,而无需修改遗留类的构造函数.
允许尽可能晚地创建昂贵的资源或服务,并且仅在需要时创建.
以下是上述代码的示例:
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;
我想既然每个人都为DI写过,我想问几个问题.
当你有一个DI的配置,其中所有实际的实现(而不是接口)将被注入一个类(例如服务到控制器)为什么不是某种硬编码?
如果我想在运行时更改对象怎么办?例如,我的配置已经说明当我实例化MyController时,将FileLogger注入ILogger.但我可能想注入DatabaseLogger.
每次我想要更改我的AClass需要的对象时,我现在需要查看两个地方 - 类本身和配置文件.这怎么能让生活更轻松?
如果没有注入AClass的Aproperty,那么嘲笑它是否更难?
回到第一个问题.如果使用new object()是坏的,我们怎么注入实现而不是接口?我想你们很多人都在说我们实际上正在注入接口,但配置会让你指定该接口的实现.不在运行时..它在编译期间是硬编码的.
这是基于@Adam N发布的答案.
为什么PersonService不再需要担心GroupMembershipService?您刚才提到GroupMembership有多个依赖的东西(对象/属性).如果PService中需要GMService,您可以将其作为属性.无论你是否注射它,你都可以嘲笑它.我唯一希望它被注入的是GMService是否有更具体的子类,直到运行时才能知道.然后你想要注入子类.或者,如果您想将其用作单身或原型.说实话,配置文件的所有内容都是硬编码的,只要它在编译时要注入的类型(接口)的子类.
编辑
Jose Maria Arranz对DI的好评
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,它执行a
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
美分
我知道已有很多答案,但我发现这非常有用: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
实例的任何类提供.
依赖注入是通常被称为"依赖性混淆"要求的一种可能的解决方案.依赖性混淆是一种将"明显"性质从提供依赖性到需要它的类的过程中带走的方法,并因此以某种方式模糊化对所述类的所述依赖性的提供.这不一定是坏事.事实上,通过模糊向类提供依赖的方式,类之外的东西负责创建依赖,这意味着,在各种场景中,可以向类提供不同的依赖实现,而不进行任何更改上课.这非常适合在生产和测试模式之间切换(例如,使用'模拟'服务依赖性).
不幸的是,糟糕的是,有些人认为你需要一个专门的框架,这样做的依赖模糊处理,你是莫名其妙地"较小"的程序员,如果你选择不使用特定的框架来做到这一点.许多人认为另一个非常令人不安的神话是依赖注入是实现依赖性混淆的唯一方法.这显然是历史性的,显然是100%错误的,但是你会难以说服某些人为依赖性混淆要求提供依赖注入的替代方案.
程序员已经理解了多年来的依赖性混淆要求,并且在构思依赖性注入之前和之后已经发展了许多替代解决方案.有工厂模式,但是使用ThreadLocal还有很多选项,不需要注入特定实例 - 依赖项有效地注入到线程中,这有利于使对象可用(通过方便的静态getter方法)到任何类需要它而不必向需要它的类添加注释,并设置复杂的XML"粘合剂"来实现它.当持久性需要依赖项(JPA/JDO或其他)时,它允许您更容易实现"转换持久性",并且域模型和业务模型类完全由POJO组成(即没有特定于框架/在注释中锁定).
来自本书,'有良好基础的Java开发人员:Java 7的重要技术和多语言编程
DI是IoC的一种特殊形式,其中查找依赖关系的过程超出了当前正在执行的代码的直接控制.