已经发布了几个关于依赖注入的具体问题,例如何时使用它以及它有什么框架,
什么是依赖注入,何时/为什么应该或不应该使用它?
已经发布了几个关于依赖注入的具体问题,例如何时使用它以及它有什么框架,
什么是依赖注入,何时/为什么应该或不应该使用它?
当前回答
让我们想象一下,你想去钓鱼:
没有依赖注入,你需要自己处理所有的事情。你需要找一艘船,买一根鱼竿,寻找诱饵等等。当然,这是可能的,但这给你带来了很多责任。在软件方面,这意味着你必须对所有这些东西进行查找。通过依赖注入,其他人负责所有准备工作,并为您提供所需的设备。你将收到(“被注射”)船、鱼竿和鱼饵——所有这些都准备好使用。
其他回答
什么是依赖注入(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来投射这些是很好的。
来自Book Apress.Spring.Persistence.with.HHibernate,2010年10月
依赖注入的目的是将解决应用程序业务中的外部软件组件逻辑。如果没有依赖注入访问所需的服务可能会与组件的密码这不仅增加了出错的可能性,还增加了代码膨胀,并放大了维护复杂性;它耦合组件更紧密地结合在一起,使得在重构或测试。
依赖注入是一种实践,其中对象的设计方式是从其他代码段接收对象实例,而不是在内部构造它们。这意味着可以在不更改代码的情况下替换实现对象所需接口的任何对象,这简化了测试,并改进了去耦。
例如,考虑这些类:
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的实例才能完成其工作。如果没有依赖注入,传统的方法是在PersonService的构造函数中实例化一个新的GroupMembershipService,并在两个函数中使用该实例属性。但是,如果GroupMembershipService的构造函数有多个它需要的东西,或者更糟的是,需要在GroupMembershipServices上调用一些初始化“setter”,代码增长相当快,PersonService现在不仅依赖于GroupMembershipService,还依赖于GroupMembershipService所依赖的所有其他东西。此外,与GroupMembershipService的链接被硬编码到PersonService中,这意味着您不能为了测试目的而“虚拟”GroupMembershipServices,或者在应用程序的不同部分使用策略模式。
使用依赖注入,而不是在PersonService中实例化GroupMembershipService,您可以将其传递给PersonService构造函数,或者添加Property(getter和setter)来设置其本地实例,并与他们合作。这也意味着,任何属于GroupMembershipService的子类或实现GroupMembershipServices接口的内容都可以“注入”到PersonService中,PersonService不需要知道更改。
什么是依赖注入?
依赖注入(DI)意味着分离彼此依赖的对象。假设对象A依赖于对象B,因此想法是将这些对象彼此分离。我们不需要使用new关键字对对象进行硬编码,而是在运行时共享对对象的依赖关系,而不管编译时间如何。如果我们谈论
依赖注入在Spring中的工作原理:
我们不需要使用new关键字硬编码对象,而是在配置文件中定义bean依赖关系。弹簧容器将负责连接所有部件。
控制反转(IOC)
IOC是一个通用概念,可以用多种不同的方式表达,依赖注入是IOC的一个具体例子。
两种类型的依赖注入:
构造器注入沉淀剂注入
1.基于构造函数的依赖注入:
当容器调用具有多个参数的类构造函数时,就完成了基于构造函数的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>
2.基于Setter的依赖注入:
基于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,这是一个很好的经验法则。注意,如果我们在setter上使用基于@Required的注释,则可以将setter作为必需的依赖项。
依赖注入(DI)是依赖反转原理(DIP)实践的一部分,也称为控制反转(IoC)。基本上,你需要做DIP,因为你想让你的代码更加模块化和单元可测试,而不是仅仅一个单片系统。因此,您开始识别可以从类中分离并抽象出来的代码部分。现在抽象的实现需要从类外部注入。通常这可以通过构造函数完成。因此,您创建了一个构造函数,它接受抽象作为参数,这称为依赖注入(通过构造函数)。有关DIP、DI和IoC容器的更多说明,请阅读此处