已经发布了几个有关具体问题的问题 依赖注入比如何时使用它以及它的框架是什么。然而,
什么是依赖注入以及何时/为什么应该或不应该使用它?
已经发布了几个有关具体问题的问题 依赖注入比如何时使用它以及它的框架是什么。然而,
什么是依赖注入以及何时/为什么应该或不应该使用它?
基本上,不是让对象创建依赖项或要求工厂对象为它们创建一个依赖项,而是将所需的依赖项传递给外部对象,并使其成为别人的问题。这个“某人”要么是依赖图之上的对象,要么是构建依赖图的依赖注入器(框架)。我在这里使用它的依赖是当前对象需要持有引用的任何其他对象。
依赖注入的一个主要优点是它可以使测试更容易。假设您有一个对象,在其构造函数中执行以下操作:
public SomeClass() {
myObject = Factory.getObject();
}
当你想要做的只是在SomeClass上运行一些单元测试时,这可能很麻烦,特别是如果myObject是复杂磁盘或网络访问的话。所以现在你正在考虑模仿myObject,但也以某种方式拦截工厂调用。硬。相反,将对象作为参数传递给构造函数。现在你已将问题转移到其他地方,但测试可以变得更容易。只需创建一个虚拟myObject并将其传入。构造函数现在看起来有点像:
public SomeClass (MyClass myObject) {
this.myObject = myObject;
}
这是一种依赖注入方式 - 通过构造函数。有几种机制是可能的。
当不使用依赖注入时(例如在其构造函数中执行过多工作的类等),在单元测试中隔离组件往往变得更加困难。
早在2013年,当我写这个答案时,这是一个主题 谷歌测试博客。这对我来说仍然是最大的优势,因为您可能并不总是需要在运行时设计中具有额外的灵活性(例如,对于服务定位器或类似的模式),但是您通常需要能够在测试期间隔离您的类。
到目前为止我发现的最佳定义是 一个是詹姆斯肖尔:
“依赖注入”是一个25美元 5美分概念的术语。 [...] 依赖注入意味着给予 对象的实例变量。 [...]。
有 马丁福勒的一篇文章 这可能也很有用。
依赖注入基本上是提供对象所需的对象(它的依赖关系),而不是让它自己构造它们。它是一种非常有用的测试技术,因为它允许模拟或删除依赖项。
可以通过多种方式(例如构造函数注入或setter注入)将依赖项注入到对象中。甚至可以使用专门的依赖注入框架(例如Spring)来做到这一点,但它们当然不是必需的。您不需要这些框架具有依赖注入。显式地实例化和传递对象(依赖关系)与框架注入一样好。
我发现这个有趣的例子 松耦合:
任何应用程序都由许多对象组成,这些对象彼此协作以执行一些有用的东西。传统上,每个对象都负责获取它自己对与之协作的依赖对象(依赖项)的引用。这导致高度耦合的类和难以测试的代码。
例如,考虑一个 Car
目的。
一个 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
在构造函数中 PersonService
并在两个函数中使用该实例属性。但是,如果构造函数 GroupMembershipService
它需要多个东西,或者更糟糕的是,有一些初始化的“setter”需要在它上面调用 GroupMembershipService
,代码增长相当快,而且 PersonService
现在不仅取决于 GroupMembershipService
还有其他一切 GroupMembershipService
依赖于取决于。而且,联系到 GroupMembershipService
是硬编码的 PersonService
这意味着你不能“假装”了 GroupMembershipService
用于测试目的,或在应用程序的不同部分使用策略模式。
使用依赖注入,而不是实例化 GroupMembershipService
在你的 PersonService
,你要么把它传递给 PersonService
构造函数,或者添加一个Property(getter和setter)来设置它的本地实例。这意味着你的 PersonService
再也不用担心如何创建了 GroupMembershipService
,它只接受它给出的那些,并与它们一起工作。这也意味着任何属于它的子类 GroupMembershipService
,或实现 GroupMembershipService
界面可以“注入” PersonService
,和 PersonService
不需要知道变化。
接受的答案是一个很好的答案 - 但我想补充一点,DI非常类似于代码中经典的避免硬编码常量。
当您使用某个常量(如数据库名称)时,您可以快速将其从代码内部移动到某个配置文件,并将包含该值的变量传递到需要它的位置。这样做的原因是这些常量通常比其余代码更频繁地更改。例如,如果您想测试测试数据库中的代码。
DI在面向对象编程领域类似于此。那里的值而不是常量文字是完整的对象 - 但是从类代码中移出代码的原因是相似的 - 对象的更改频率比使用它们的代码更频繁。需要进行此类更改的一个重要案例是测试。
让我们想象你想钓鱼:
没有依赖注入,你需要自己处理所有事情。你需要找一条船,买一根钓竿,寻找诱饵等等。当然,这可能会给你带来很多责任。在软件方面,这意味着您必须对所有这些事情执行查找。
通过依赖注入,其他人负责所有准备工作并为您提供所需的设备。您将收到(“注入”)船,钓竿和鱼饵 - 所有这些都可以使用。
这个 是最简单的解释 依赖注入 和 依赖注入容器 我见过:
依赖注入 和 依赖注入容器 有不同的东西:
您不需要容器来执行依赖项注入。但是容器可以帮助您。
让我们试试一下简单的例子吧 汽车 和 发动机 班级,任何汽车都需要发动机才能到达任何地方,至少现在如此。所以下面代码看起来如何没有依赖注入。
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类。
主要的好处是 依赖注入 这些类更松散耦合,因为它们没有硬编码的依赖项。这遵循上面提到的依赖性倒置原则。类不是引用特定的实现,而是请求抽象(通常 接口)在课程建设时提供给他们的。
所以最后 依赖注入 只是一种技术 实现对象及其依赖项之间的松散耦合。 而不是直接实例化类所需的依赖项 为了执行其操作,将依赖关系提供给类 (最常见的)通过构造函数注入。
此外,当我们有很多依赖项时,使用Inversion of Control(IoC)容器是非常好的做法,我们可以告诉哪些接口应该映射到我们所有依赖项的哪些具体实现,并且我们可以让它在构造时为我们解析这些依赖项我们的目标。例如,我们可以在IoC容器的映射中指定 IEngine 依赖应该映射到 GasEngine 当我们向IoC容器询问我们的实例时 汽车 类,它会自动构造我们的 汽车 有一个班级 GasEngine 依赖传入。
更新: 最近看过Julie Lerman关于EF Core的课程,也喜欢她关于DI的简短定义。
依赖注入是一种允许应用程序注入的模式 将对象移动到需要它们的类,而不强制它们 要负责这些对象的类。它允许您的代码 更松散耦合,实体框架核心插入到这一点 服务体系。
“依赖注入”不仅仅意味着使用参数化构造函数和公共setter吗?
没有依赖注入的构造函数:
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();
...
}
}
什么是依赖注入(DI)?
正如其他人所说, 依赖注入(DI) 消除了我们感兴趣的类(消费者类)所依赖的其他对象实例的直接创建和生命管理的责任(在 UML感)。这些实例通常作为构造函数参数或通过属性设置器传递给我们的使用者类(依赖对象实例化和传递给使用者类的管理通常由 控制反转(IoC) 容器,但这是另一个话题)。
DI,DIP和SOLID
具体来说,在Robert C Martin的范例中 面向对象设计的SOLID原则, DI
是可能的实现之一 依赖倒置原则(DIP)。该 DIP是 D
的 SOLID
口头禅 - 其他DIP实现包括服务定位器和插件模式。
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
在工厂根据需要,并在完成后处理这些实例。何时使用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());
}
下一步
依赖注入总是与a相关联 控制容器(IoC)的反转,注入(提供)具体的依赖实例,并管理生命周期实例。在配置/引导过程中, IoC
容器允许定义以下内容:
IBar
,返回一个 ConcreteBar
例如”)IDisposable
并将承担责任 Disposing
依赖性与配置的生命周期管理一致。通常,一旦配置/引导IoC容器,它们就会在后台无缝运行,允许编码器专注于手头的代码,而不是担心依赖性。
DI友好代码的关键是避免类的静态耦合,而不是使用new()来创建依赖项
按照上面的例子,依赖关系的解耦确实需要一些设计工作,对于开发人员来说,需要一种范式转换来打破习惯。 new
直接依赖于依赖关系,而是信任容器来管理依赖关系。
但好处很多,特别是在彻底测试你感兴趣的课程的能力。
注意 :创建/映射/投影(通过 new ..()
)POCO / POJO /序列化DTO /实体图/匿名JSON投影等 - 即“仅数据”类或记录 - 从方法使用或返回 不 被视为依赖(在UML意义上)并且不受DI的影响。运用 new
投射这些就好了。