已经发布了几个关于依赖注入的具体问题,例如何时使用它以及它有什么框架,
什么是依赖注入,何时/为什么应该或不应该使用它?
已经发布了几个关于依赖注入的具体问题,例如何时使用它以及它有什么框架,
什么是依赖注入,何时/为什么应该或不应该使用它?
当前回答
简单来说,依赖注入(DI)是消除不同对象之间的依赖关系或紧密耦合的方法。依赖注入为每个对象提供一个内聚行为。
DI是国际奥委会春季原则的实施,该原则说“不要打电话给我们,我们会打电话给你”。使用依赖注入程序员不需要使用new关键字创建对象。
对象一旦加载到Spring容器中,我们就可以在需要时重用它们,方法是使用getBean(StringbeanName)方法从Spring容器中获取这些对象。
其他回答
DI是真实对象之间实际交互的方式,而不需要一个对象负责另一个对象的存在。应平等对待对象。它们都是对象。任何人都不应该表现得像一个创造者。这就是你如何公正对待你的目标。
简单示例:
如果你需要医生,你只需去找一位(现有的)医生。你不会考虑从头开始创建一个医生来帮助你。他已经存在,他可能为你或其他对象服务。无论你(一个物体)是否需要他,他都有权存在,因为他的目的是为一个或多个物体服务。决定他的存在的是全能的上帝,而不是自然选择。因此,DI的一个优点是避免在整个宇宙(即应用程序)的生命周期中创建无用的冗余对象。
来自Book Apress.Spring.Persistence.with.HHibernate,2010年10月
依赖注入的目的是将解决应用程序业务中的外部软件组件逻辑。如果没有依赖注入访问所需的服务可能会与组件的密码这不仅增加了出错的可能性,还增加了代码膨胀,并放大了维护复杂性;它耦合组件更紧密地结合在一起,使得在重构或测试。
我在松耦合方面发现了一个有趣的例子:
来源:了解依赖注入
任何应用程序都由许多对象组成,这些对象相互协作以执行一些有用的任务。传统上,每个对象都负责获取自己对与其协作的依赖对象(依赖关系)的引用。这导致了高度耦合的类和难以测试的代码。
例如,考虑Car对象。
汽车依靠轮子、发动机、燃料、电池等运转。传统上,我们定义此类依赖对象的品牌以及Car对象的定义。
无依赖注入(DI):
class Car{
private Wheel wh = new NepaliRubberWheel();
private Battery bt = new ExcideBattery();
//The rest
}
在这里,Car对象负责创建从属对象。
如果我们希望在初始NepaliRubberWheel()穿孔后更改其从属对象的类型(例如Wheel),该怎么办?我们需要重新创建Car对象及其新的依赖项,例如ChineseRubberWheel(),但只有Car制造商才能做到这一点。
那么依赖注入为我们做了什么。。。?
当使用依赖注入时,对象在运行时而不是编译时(汽车制造时)被赋予依赖性。因此,我们现在可以随时更改轮子。在这里,相关性(轮子)可以在运行时注入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;
}
}
其优点是:
分离对象的创建(换句话说,将使用与对象的创建分开)能够替换依赖项(例如:车轮、电池),而不改变使用它的类(汽车)促进“代码到接口而不是实现”原则在测试期间创建和使用模拟依赖关系的能力(如果我们想在测试期间使用模拟轮而不是真实实例,我们可以创建模拟轮对象并让DI框架注入Car)
例如,我们有两类客户机和服务。客户端将使用服务
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();
优势
易于理解的
缺点
难以测试客户端类当我们更改Service构造函数时,我们需要在所有位置更改代码createService对象
使用依赖注入
方式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
===
现在,这段代码已经遵循了依赖注入,测试客户端类更容易。然而,我们仍然多次使用新的Service(),并且在更改Service构造函数时效果不佳。为了防止这种情况,我们可以使用DI注射器1) 简单手动喷油器
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
优势
使测试更容易更改服务时,只需在Injector类中更改如果您使用使用构造函数注入,当您查看Client的构造函数时,您将看到Client类有多少依赖项
缺点
如果使用构造函数注入,则在创建客户端时创建服务对象,有时我们在客户端类中使用函数而不使用服务,因此创建的服务被浪费
依赖注入定义
https://en.wikipedia.org/wiki/Dependency_injection
依赖项是可以使用的对象(服务)注入是将依赖项(Service)传递给将使用它的依赖对象(Client)
什么是依赖注入(DI)?
正如其他人所说,依赖注入(DI)消除了直接创建和管理我们感兴趣的类(消费者类)所依赖的其他对象实例(在UML意义上)的生命周期的责任。这些实例通常作为构造函数参数或通过属性设置器传递给我们的消费者类(依赖对象实例化和传递给消费者类的管理通常由控制反转(IoC)容器执行,但这是另一个主题)。
DI、DIP和固体
具体来说,在Robert C Martin的面向对象设计的SOLID原则的范例中,DI是依赖反转原则(DIP)的可能实现之一。DIP是SOLID咒语的D——其他DIP实现包括服务定位器和插件模式。
DIP的目标是解耦类之间紧密、具体的依赖关系,相反,通过抽象来放松耦合,这可以通过接口、抽象类或纯虚拟类来实现,具体取决于所使用的语言和方法。
如果没有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);
}
}
虽然看似无害,但它对另外两个类System.DateTime和System.Console有两个静态依赖关系,这不仅限制了日志输出选项(如果没有人在监视,则日志记录到控制台将毫无价值),而且更糟糕的是,考虑到对非确定性系统时钟的依赖关系,很难自动测试。
然而,我们可以将DIP应用于这个类,方法是将时间戳问题抽象为依赖项,并将MyLogger仅耦合到一个简单的接口:
public interface IClock
{
DateTime Now { get; }
}
我们还可以将对Console的依赖放宽为抽象,例如TextWriter。依赖注入通常实现为构造函数注入(将抽象作为参数传递给依赖项作为消费类的构造函数)或Setter注入(通过setXyz()Setter或定义了{set;}的.Net Property传递依赖项)。构造函数注入是首选的,因为这样可以保证类在构造后处于正确的状态,并允许将内部依赖字段标记为只读(C#)或最终(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。现在,这两个依赖关系需要由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实例”)可以为每个依赖项的生命周期管理设置策略,例如为每个消费者实例创建新对象,在所有消费者之间共享单一依赖项实例,仅在同一线程之间共享同一依赖项实例等。在.Net中,IoC容器了解IDisposable等协议,并将根据配置的生命周期管理来负责处理依赖关系。
通常,一旦IoC容器被配置/引导,它们就可以在后台无缝地运行,从而让编码器专注于手头的代码,而不用担心依赖性。
DI友好代码的关键是避免类的静态耦合,并且不要使用new()创建依赖项
根据上面的示例,依赖关系的解耦确实需要一些设计工作,对于开发人员来说,需要进行范式转换,以打破直接添加依赖关系的习惯,转而信任容器来管理依赖关系。
但好处很多,特别是能够彻底测试你感兴趣的班级。
注意:POCO/POJO/Serialization DTO/Entity Graphs/Anonymous JSON投影等(即“仅数据”类或记录)的创建/映射/投影(通过新的..())不被视为依赖项(在UML意义上),也不受DI的约束。使用new来投射这些是很好的。