已经发布了几个关于依赖注入的具体问题,例如何时使用它以及它有什么框架,

什么是依赖注入,何时/为什么应该或不应该使用它?


当前回答

我将提出一个稍微不同的、简短而精确的依赖注入定义,侧重于主要目标,而不是技术手段(从这里开始):

依赖注入是创建静态、无状态的服务对象图,其中每个服务由其依赖关系。

我们在应用程序中创建的对象(无论我们是否使用Java、C#或其他面向对象语言)通常分为两类:无状态、静态和全局“服务对象”(模块),以及有状态、动态和本地“数据对象”。

模块图(服务对象图)通常在应用程序启动时创建。这可以使用容器(如Spring)完成,但也可以通过向对象构造函数传递参数来手动完成。这两种方法都有其优点和缺点,但在应用程序中使用DI肯定不需要框架。

一个要求是服务必须通过其依赖性进行参数化。这意味着什么完全取决于给定系统中采用的语言和方法。通常,这采用构造函数参数的形式,但使用setter也是一种选择。这也意味着(在调用服务方法时)对服务的用户隐藏服务的依赖关系。

何时使用?我会说,每当应用程序足够大时,将逻辑封装到单独的模块中,在模块之间使用依赖关系图,可以提高代码的可读性和可探索性。

其他回答

例如,我们有两类客户机和服务。客户端将使用服务

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)

依赖注入是将依赖传递给其他对象或框架(依赖注入器)。

依赖注入使测试更容易。注入可以通过构造函数完成。

SomeClass()的构造函数如下:

public SomeClass() {
    myObject = Factory.getObject();
}

问题:如果myObject涉及诸如磁盘访问或网络访问之类的复杂任务,则很难在SomeClass()上进行单元测试。程序员必须模拟myObject,并可能拦截工厂调用。

替代解决方案:

将myObject作为参数传入构造函数

public SomeClass (MyClass myObject) {
    this.myObject = myObject;
}

myObject可以直接传递,这使得测试更容易。

一种常见的替代方法是定义一个不做任何事情的构造函数。依赖注入可以通过setter完成。(h/t@MikeVella)。Martin Fowler记录了第三种选择(h/t@MarcDix),其中类显式地实现了程序员希望注入的依赖项的接口。

在没有依赖注入的情况下,很难在单元测试中隔离组件。

2013年,当我写下这个答案时,这是谷歌测试博客的一个主要主题。这对我来说仍然是最大的优势,因为程序员在运行时设计中并不总是需要额外的灵活性(例如,服务定位器或类似模式)。程序员通常需要在测试期间隔离类。

依赖注入是一种实践,其中对象的设计方式是从其他代码段接收对象实例,而不是在内部构造它们。这意味着可以在不更改代码的情况下替换实现对象所需接口的任何对象,这简化了测试,并改进了去耦。

例如,考虑这些类:

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不需要知道更改。

依赖注入是解决“依赖混淆”需求的一种可能方案。依赖性混淆是一种将“明显”性质从向需要依赖性的类提供依赖性的过程中去除的方法,因此在某种程度上混淆了向所述类提供所述依赖性。这不一定是坏事。事实上,通过混淆向类提供依赖项的方式,类外部的某个东西负责创建依赖项,这意味着在各种情况下,可以向类提供不同的依赖项实现,而不需要对类进行任何更改。这对于在生产和测试模式之间切换非常有用(例如,使用“模拟”服务依赖)。

不幸的是,糟糕的部分是,有些人认为你需要一个专门的框架来进行依赖性混淆,如果你选择不使用特定的框架来做,那么你在某种程度上就是一个“低级”程序员。另一个非常令人不安的神话是,依赖性注入是实现依赖性混淆的唯一方法。这显然是历史性的,显然是100%错误的,但你很难说服一些人,依赖项注入可以替代依赖项混淆需求。

多年来,程序员们已经了解了依赖性混淆的需求,在考虑依赖性注入之前和之后,许多替代解决方案都已经发展起来。有工厂模式,但也有许多使用ThreadLocal的选项,其中不需要对特定实例进行注入-依赖关系被有效地注入到线程中,这样做的好处是使对象(通过方便的静态getter方法)可用于任何需要它的类,而无需向需要它的类别添加注释并设置复杂的XML“粘合”以实现这一点。当持久性需要依赖项(JPA/JDO或其他)时,它允许您更容易地实现“跨持久性”,并且域模型和业务模型类完全由POJO组成(即没有特定于框架的/锁定在注释中的)。

我想既然每个人都为DI写过文章,让我问几个问题。。

当您有一个DI配置,其中所有实际实现(而不是接口)都将被注入到一个类中(例如控制器的服务),为什么这不是某种硬编码?如果我想在运行时更改对象怎么办?例如,我的配置已经表明,当我实例化MyController时,为FileLogger注入ILogger。但我可能想注入DatabaseLogger。每次我想更改AClass需要的对象时,我都需要查看两个地方——类本身和配置文件。这是如何让生活更轻松的?如果没有注入Aproperty的AClass,是否更难模拟?回到第一个问题。如果使用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和注释也是源代码)。问题是这种锅炉板减少在非常简单的情况下是真实的(每个类一个实例以及类似的情况),有时在现实世界中,选择适当的服务对象不像将类映射到单个对象那样容易。