已经有一些关于依赖注入的问题被提出,例如何时使用它以及有哪些框架可用。然而,
什么是依赖注入?何时/为什么应该或不应该使用它?
已经有一些关于依赖注入的问题被提出,例如何时使用它以及有哪些框架可用。然而,
什么是依赖注入?何时/为什么应该或不应该使用它?
到目前为止我找到的最好的定义是James Shore提出的:
"依赖注入"是一个价值25美元的术语,用来描述一个价值5美分的概念。[...] 依赖注入意味着给对象其实例变量。[...]
Martin Fowler撰写的一篇文章也可能会有所帮助。
依赖注入基本上是提供对象需要的对象(即其依赖项),而不是让它自己构造这些对象。这是一种非常有用的测试技术,因为它允许依赖项被模拟或存根掉。
依赖项可以通过多种方式(如构造函数注入或设置器注入)注入到对象中。甚至可以使用专门的依赖注入框架(例如Spring)来完成这个过程,但它们并不是必需的。你不需要这些框架来进行依赖注入。显式地实例化和传递对象(依赖项)同样可以完成注入的过程。
依赖注入(Dependency Injection)是指将依赖传递给其他对象或框架(依赖注入器)。
依赖注入使得测试变得更加容易。可以通过构造函数进行注入。
SomeClass()
的构造函数如下:
public SomeClass() {
myObject = Factory.getObject();
}
问题:
如果myObject
涉及到复杂任务,如磁盘访问或网络访问,则在SomeClass()
上进行单元测试是困难的。程序员必须模拟myObject
并可能拦截工厂调用。
替代方案:
myObject
作为参数传递给构造函数public SomeClass (MyClass myObject) {
this.myObject = myObject;
}
myObject
可以直接传递,这使得测试更加容易。
如果没有依赖注入,单元测试中隔离组件就会更加困难。
在我撰写本答案时的2013年,这是Google测试博客上的主题。对我来说,这仍然是最大的优势,因为程序员并不总是需要运行时设计中的额外灵活性(例如,对于服务定位器或类似模式)。程序员通常需要在测试期间隔离类。
我在关于松耦合的维基百科上发现了这个有趣的例子:
来源:理解依赖注入
任何应用都由许多对象协作完成一些有用的事情。传统上,每个对象负责获取与之协作的依赖对象(依赖项)的引用。这导致类之间高度耦合和难以测试的代码。
例如,考虑一个Car
对象。
一辆汽车需要轮子、发动机、燃料、电池等等才能运行。传统上,我们在Car
对象的定义中同时定义这些依赖对象的品牌。
没有依赖注入(DI):
class Car{
private Wheel wh = new NepaliRubberWheel();
private Battery bt = new ExcideBattery();
//The rest
}
在这里,Car
对象负责创建依赖对象。
如果我们想在最初的NepaliRubberWheel()
失效后更改其依赖对象类型 - 比如Wheel
- 怎么办呢?
我们需要用新的依赖ChineseRubberWheel()
重新创建Car
对象,但只有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;
}
}
优点:
依赖注入(Dependency Injection)是一种实践,它使对象以一种方式设计,从其他代码片段中接收对象的实例而不是在内部构造它们。这意味着任何实现所需接口的对象都可以替换进来,而无需更改代码,这简化了测试并提高了解耦性。
例如,请考虑以下类:
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
的构造函数有多个要求的参数,或者更糟糕的是,有一些需要调用GroupMembershipService
的初始化“setter”的情况,代码会很快变得臃肿,并且PersonService
不仅依赖于GroupMembershipService
,还依赖于GroupMembershipService
所依赖的一切。此外,与GroupMembershipService
的链接被硬编码到PersonService
中,这意味着您无法为测试目的“模拟”GroupMembershipService
,或在应用程序的不同部分中使用策略模式。PersonService
内部实例化GroupMembershipService
,您可以将其传递给PersonService
构造函数,或者添加一个属性(getter和setter)来设置其本地实例。这意味着您的PersonService
不再需要担心如何创建GroupMembershipService
,它只接受给定的对象,并与它们一起工作。这也意味着任何是GroupMembershipService
子类或实现了GroupMembershipService
接口的东西都可以被“注入”到PersonService
中,而PersonService
并不需要知道这种变化。这个被接受的答案是不错的,但我想要补充的是,依赖注入非常类似于经典的避免在代码中硬编码常量。
当您使用某些常量比如数据库名称时,您很快会将其从代码内部移动到一些配置文件中,并传递一个包含该值的变量到需要它的地方。做这件事的原因是,这些常量通常比代码的其他部分更频繁地发生更改。例如,如果您想在测试数据库中测试代码。
在面向对象编程的世界中,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类。随着应用程序越来越大,使用新类型的引擎会带来更多问题和头痛。 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的实例并给它一个真正的具体引擎类,比如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类,当我们请求我们的Car类的实例时,它将自动使用传递给它的GasEngine依赖项构造我们的Car类。
更新: 最近观看了Julie Lerman发布的有关EF Core的课程,并喜欢她对DI的简短定义。
依赖注入是一种模式,允许您的应用程序在需要它们的类上动态注入对象,而不会强制那些类负责这些对象。它使代码的耦合度更低,Entity Framework Core也插入到这个系统服务中。
假设你想去钓鱼:
没有依赖注入,你需要自己处理一切。你需要找船、买渔竿、找饵料等等。当然这是可能的,但这会让你承担很多责任。在软件术语中,这意味着你需要执行查找所有这些事情。
有了依赖注入,其他人将负责所有的准备工作,并为你提供所需的设备。你将被注入("be injected")船只、渔竿和饵料-全部准备就绪。
这里是我见过的关于依赖注入(Dependency Injection)和依赖注入容器(Dependency Injection Container)最简单的解释:
依赖注入(Dependency Injection)和依赖注入容器(Dependency Injection Container)是不同的东西:
您不需要容器来进行依赖注入。但是容器可以帮助您。
现在在技术世界中,依赖注入是一种技术,其中一个对象(或静态方法)提供另一个对象的依赖项。因此,将创建对象的任务转移给其他人并直接使用依赖项称为依赖注入。
“依赖注入”不就是使用带参数的构造函数和公共设置器吗?
Constructor without dependency injection:
public class Example { private DatabaseThingie myDatabase; public Example() { myDatabase = new DatabaseThingie(); } public void doStuff() { ... myDatabase.getData(); ... } }
Constructor with dependency injection:
public class Example { private DatabaseThingie myDatabase; public Example(DatabaseThingie useThisDatabaseInstead) { myDatabase = useThisDatabaseInstead; } public void doStuff() { ... myDatabase.getData(); ... } }
new DatabaseThingie()
不能生成有效的myDatabase实例时。 - JaneGoodall