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

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


当前回答

我想既然每个人都为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和注释也是源代码)。问题是这种锅炉板减少在非常简单的情况下是真实的(每个类一个实例以及类似的情况),有时在现实世界中,选择适当的服务对象不像将类映射到单个对象那样容易。

其他回答

什么是依赖注入(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来投射这些是很好的。

让我们想象一下,你想去钓鱼:

没有依赖注入,你需要自己处理所有的事情。你需要找一艘船,买一根鱼竿,寻找诱饵等等。当然,这是可能的,但这给你带来了很多责任。在软件方面,这意味着你必须对所有这些东西进行查找。通过依赖注入,其他人负责所有准备工作,并为您提供所需的设备。你将收到(“被注射”)船、鱼竿和鱼饵——所有这些都准备好使用。

依赖注入是与Spring框架相关概念的核心。在创建任何项目的框架时,Spring都可能发挥重要作用,而依赖注入就是其中之一。

实际上,假设在java中,您创建了两个不同的类,即类A和类B,并且无论类B中有什么函数,您都希望在类A中使用,所以此时可以使用依赖注入。在这里,您可以将一个类的对象放入另一个类中,就像您可以将整个类注入另一个类别中以使其可访问一样。通过这种方式可以克服依赖性。

依赖注入只是将两个类粘合在一起,同时保持它们的分离。

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

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)是设计模式中的一种,它使用了OOP的基本特性——一个对象与另一个对象之间的关系。虽然继承继承一个对象以实现更复杂和更具体的另一个对象,但关系或关联只需使用属性从一个对象创建指向另一对象的指针。DI的功能与OOP的其他特性相结合,如接口和隐藏代码。假设图书馆里有一个客户(订阅者),为了简单起见,他只能借一本书。

书本界面:

package com.deepam.hidden;

public interface BookInterface {

public BookInterface setHeight(int height);
public BookInterface setPages(int pages);   
public int getHeight();
public int getPages();  

public String toString();
}

接下来我们可以有很多种书;其中一种类型是虚构:

package com.deepam.hidden;

public class FictionBook implements BookInterface {
int height = 0; // height in cm
int pages = 0; // number of pages

/** constructor */
public FictionBook() {
    // TODO Auto-generated constructor stub
}

@Override
public FictionBook setHeight(int height) {
  this.height = height;
  return this;
}

@Override
public FictionBook setPages(int pages) {
  this.pages = pages;
  return this;      
}

@Override
public int getHeight() {
    // TODO Auto-generated method stub
    return height;
}

@Override
public int getPages() {
    // TODO Auto-generated method stub
    return pages;
}

@Override
public String toString(){
    return ("height: " + height + ", " + "pages: " + pages);
}
}

现在,用户可以与图书建立关联:

package com.deepam.hidden;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Subscriber {
BookInterface book;

/** constructor*/
public Subscriber() {
    // TODO Auto-generated constructor stub
}

// injection I
public void setBook(BookInterface book) {
    this.book = book;
}

// injection II
public BookInterface setBook(String bookName) {
    try {
        Class<?> cl = Class.forName(bookName);
        Constructor<?> constructor = cl.getConstructor(); // use it for parameters in constructor
        BookInterface book = (BookInterface) constructor.newInstance();
        //book = (BookInterface) Class.forName(bookName).newInstance();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (SecurityException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
    return book;
}

public BookInterface getBook() {
  return book;
}

public static void main(String[] args) {

}

}

这三个类都可以隐藏起来,以便实现自己的功能。现在我们可以将此代码用于DI:

package com.deepam.implement;

import com.deepam.hidden.Subscriber;
import com.deepam.hidden.FictionBook;

public class CallHiddenImplBook {

public CallHiddenImplBook() {
    // TODO Auto-generated constructor stub
}

public void doIt() {
    Subscriber ab = new Subscriber();

    // injection I
    FictionBook bookI = new FictionBook();
    bookI.setHeight(30); // cm
    bookI.setPages(250);
    ab.setBook(bookI); // inject
    System.out.println("injection I " + ab.getBook().toString());

    // injection II
    FictionBook bookII = ((FictionBook) ab.setBook("com.deepam.hidden.FictionBook")).setHeight(5).setPages(108); // inject and set
    System.out.println("injection II " + ab.getBook().toString());      
}

public static void main(String[] args) {
    CallHiddenImplBook kh = new CallHiddenImplBook();
    kh.doIt();
}
}

如何使用依赖注入有许多不同的方法。可以将它与Singleton等结合起来,但基本上它只是通过在另一个对象内创建对象类型的属性来实现的关联。它的有用性是唯一的,也是唯一的特点,我们应该反复编写的代码总是为我们准备好并做好准备。这就是为什么DI如此紧密地与控制反转(IoC)绑定,这意味着我们的程序将控制传递给另一个正在运行的模块,该模块将bean注入到我们的代码中。(可以被注入的每个对象都可以被签名或被认为是一个Bean。)例如,在Spring中,它是通过创建和初始化ApplicationContext容器来完成的,这对我们来说很有用。我们只需在代码中创建Context并调用初始化Bean。此时注射已自动完成。