为什么使用依赖注入?

时间:2013-01-13 06:59:56

标签: dependency-injection

我正在努力理解dependency injections(DI),我又一次失败了。这看起来很傻。我的代码从来都不是一团糟;我几乎没有编写虚函数和接口(虽然我曾经在蓝色月亮中做过)并且我的所有配置都是使用json.net神奇地序列化为一个类(有时使用XML序列化器)。

我不太明白它解决了什么问题。它看起来像是一种说法:“嗨。当你遇到这个函数时,返回一个这种类型的对象并使用这些参数/数据。”

但是......为什么我会用它呢?注意我从来不需要使用object,但我明白这是为了什么。

在构建网站或桌面应用程序时,哪些人会使用DI,有哪些实际情况?我可以轻松地提出案例,说明为什么有人可能想要在游戏中使用接口/虚拟功能,但在非游戏代码中使用它非常罕见(很少见,我记不起单个实例)。

6 个答案:

答案 0 :(得分:813)

首先,我想解释一下我为这个答案做出的假设。这并不总是正确的,但经常是:

  

接口是形容词;课程是名词。

(实际上,有些名词也是名词,但我想在这里概括一下。)

所以,例如界面可以是诸如IDisposableIEnumerableIPrintable之类的东西。类是这些接口中的一个或多个的实际实现:ListMap可能都是IEnumerable的实现。

要明白这一点:通常你的课程相互依赖。例如。你可以有一个Database类访问你的数据库(哈,惊喜!;-)),但你也希望这个类做关于访问数据库的日志。假设您有另一个班级Logger,则Database依赖Logger

到目前为止,非常好。

您可以使用以下行在Database类中对此依赖关系进行建模:

var logger = new Logger();

一切都很好。当你意识到你需要一堆记录器时,这是很好的:有时你想要登录到控制台,有时你想登录到文件系统,有时候使用TCP / IP和远程登录服务器,等等......

当然你做 NOT 想要改变你所有的代码(同时你已经知道了)并替换所有行

var logger = new Logger();

由:

var logger = new TcpLogger();

首先,这不好玩。其次,这容易出错。第三,对于训练有素的猴子来说,这是一项愚蠢的,重复性的工作。那你做什么?

显然,引入由所有各种记录器实现的接口ICanLog(或类似的)是一个非常好的主意。因此,代码中的第1步是:

ICanLog logger = new Logger();

现在类型推断不再改变类型,你总是有一个单一的界面来开发。下一步是您不希望一遍又一遍地new Logger()。因此,您可以为单个中央工厂类创建新实例,并获得以下代码:

ICanLog logger = LoggerFactory.Create();

工厂本身决定要创建哪种记录器。您的代码不再关心,如果您想更改正在使用的记录器类型,您可以将其更改为一次:在工厂内。

现在,当然,您可以对此工厂进行概括,并使其适用于任何类型:

ICanLog logger = TypeFactory.Create<ICanLog>();

此TypeFactory需要配置数据,当请求特定接口类型时,实际类要实例化,因此您需要映射。当然,您可以在代码中进行此映射,但是类型更改意味着重新编译。但你也可以将这个映射放在一个XML文件中,例如..这允许你甚至在编译时(!)之后改变实际使用的类,这意味着动态地,而不需要重新编译!

为您提供一个有用的示例:想想一个不能正常登录的软件,但是当您的客户打电话并因为遇到问题而寻求帮助时,您发送给他的只是一个更新的XML配置文件,并且现在他已启用日志记录,您的支持人员可以使用日志文件来帮助您的客户。

现在,当您稍微更换名称时,您最终会得到一个服务定位器的简单实现,这是控制反转的两种模式之一(因为你反过来控制谁决定要实例化的确切类)。

总而言之,这减少了代码中的依赖关系,但现在所有代码都依赖于中央的单一服务定位器。

依赖注入现在是这一行的下一步:只需摆脱对服务定位器的这种单一依赖:代替各种类询问服务定位器的特定接口的实现, - 再一次 - 恢复对谁实例化的控制权。

通过依赖注入,您的Database类现在有一个构造函数,它需要ICanLog类型的参数:

public Database(ICanLog logger) { ... }

现在你的数据库总是有一个记录器可供使用,但它不再知道这个记录器的来源。

这就是DI框架发挥作用的地方:您再次配置映射,然后让您的DI框架为您实例化您的应用程序。由于Application类需要ICanPersistData实现,因此会注入Database的实例 - 但为此必须首先创建为ICanLog配置的记录器类型的实例}。等等...

因此,简而言之:依赖注入是如何在代码中删除依赖关系的两种方法之一。它对于编译后的配置更改非常有用,对于单元测试来说它是一件好事(因为它可以很容易地注入存根和/或模拟)。

实际上,如果没有服务定位器,有些事情你不能做(例如,如果你事先不知道你需要多少个特定接口的实例:一个DI框架每个参数总是只注入一个实例,但是当然,你可以在循环中调用服务定位器),因此大多数情况下每个DI框架也提供服务定位器。

但基本上就是这样。

希望有所帮助。

PS:我在这里描述的是一种名为构造函数注入的技术,还有属性注入,其中没有构造函数参数,但属性用于定义和解析依赖项。将属性注入视为可选依赖项,将构造函数注入视为必需依赖项。但对此的讨论超出了这个问题的范围。

答案 1 :(得分:479)

我认为很多时候人们对依赖注入和依赖注入框架(或容器)之间的区别感到困惑通常被称为。)

依赖注入是一个非常简单的概念。而不是这段代码:

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}
你编写这样的代码:

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

就是这样。说真的。这给你带来了很多好处。两个重要的是能够从中心位置(Main()函数)控制功能,而不是在整个程序中传播它,并且能够更容易地隔离测试每个类(因为你可以通过模拟或其他伪造将对象转换为构造函数而不是实际值。)

当然,缺点是你现在有一个知道程序使用的所有类的超级函数。这就是DI框架可以提供的帮助。但是,如果您无法理解为什么这种方法很有价值,我建议首先从手动依赖注入开始,这样您就可以更好地了解各种框架可以为您做些什么。

答案 2 :(得分:35)

正如其他答案所述,依赖注入是一种在使用它的类之外创建依赖项的方法。你从外面注入它们,并从你的班级内部控制他们的创造。这也是依赖注入是Inversion of control(IoC)原则实现的原因。

IoC是原则,其中DI是模式。就我的经验而言,你可能“需要多个记录器”的原因从未真正得到满足,但实际的原因是,无论何时你测试某些东西,你真的需要它。一个例子:

我的专题:

  

当我查看优惠时,我想标记我自动查看它,以便我不会忘记这样做。

您可以像这样测试:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

所以OfferWeasel中的某个地方,它构建了一个像这样的商品对象:

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

这里的问题是,此测试很可能总是失败,因为正在设置的日期将与声明的日期不同,即使您只是将DateTime.Now放入测试代码中它可能会关闭几毫秒,因此总是会失败。现在更好的解决方案是为此创建一个接口,允许您控制将设置的时间:

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

接口是抽象。一个是真实的东西,另一个允许你假装需要它的时间。然后可以像这样更改测试:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

像这样,您通过注入依赖项(获取当前时间)来应用“控制反转”原则。这样做的主要原因是为了更容易进行隔离单元测试,还有其他方法可以做到这一点。例如,这里的接口和类是不必要的,因为在C#函数中可以作为变量传递,因此您可以使用Func<DateTime>来实现相同的接口而不是接口。或者,如果采用动态方法,只需传递具有等效方法(duck typing)的任何对象,并且根本不需要接口。

您几乎不需要多个记录器。尽管如此,依赖注入对于静态类型的代码(如Java或C#)至关重要。

并... 还应该注意,如果对象的所有依赖关系都可用,则对象只能在运行时正确地实现其目的,因此在设置属性注入时没有太大用处。在我看来,在调用构造函数时应该满足所有依赖项,因此构造函数注入是可以使用的。

我希望有所帮助。

答案 3 :(得分:12)

我认为经典的答案是创建一个更加分离的应用程序,它不知道在运行时将使用哪个实现。

例如,我们是一家中央支付提供商,与世界各地的许多支付提供商合作。但是,当提出请求时,我不知道我要打电话给哪个支付处理器。我可以使用大量的开关案例编写一个类,例如:

class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

现在想象一下,现在你需要在一个类中维护所有这些代码,因为它没有正确解耦,你可以想象对于你支持的每个新处理器,你需要创建一个新的if //为每个方法切换大小写,这只会变得更复杂,但是,通过使用依赖注入(或控制反转 - 因为它有时被称为,意味着控制程序运行的任何人只在运行时知道,而不是复杂),你可以实现非常整洁和可维护的东西。

class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

**代码无法编译,我知道:)

答案 4 :(得分:6)

使用DI的主要原因是您希望将知识的责任放在知识存在的地方。 DI的概念非常符合界面封装和设计。 如果前端从后端询问某些数据,那么前端后端如何解决该问题并不重要。这取决于requesthandler。

这在OOP中已经很常见了。很多时候创建代码片段如:

I_Dosomething x = new Impl_Dosomething();

缺点是实现类仍然是硬编码的,因此前端具有使用实现的知识。 DI通过接口进一步采用设计,前端唯一需要知道的是接口的知识。 在DYI和DI之间是服务定位器的模式,因为前端必须提供密钥(存在于服务定位器的注册表中)以使其请求得到解决。 服务定位器示例:

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

DI示例:

I_Dosomething x = DIContainer.returnThat();

DI的一个要求是容器必须能够找出哪个类是哪个接口的实现。因此,DI容器需要强类型设计,并且每个接口同时只需要一个实现。如果您需要同时实现更多接口(如计算器),则需要服务定位器或工厂设计模式。

D(b)I:依赖注入和接口设计。 这种限制虽然不是一个很大的实际问题。使用D(b)I的好处是它服务于客户端和提供者之间的通信。界面是对象或一组行为的透视图。后者在这里至关重要。

我更喜欢在编码中与D(b)I一起管理服务合同。他们应该一起去。在我的观点中,使用D(b)I作为技术解决方案而没有组织管理服务合同并不是非常有益,因为DI只是一个额外的封装层。但是当你可以将它与组织管理一起使用时,你可以真正地利用我提供的组织原则D(b)。 从长远来看,它可以帮助您与客户和其他技术部门建立联系,包括测试,版本控制和替代方案的开发。当你在硬编码类中有一个隐式接口时,那么随着时间的推移,当你使用D(b)I将其显式化时,它的可通信性要小得多。这一切都归结为维护,这是随着时间的推移,而不是一次。 : - )

答案 5 :(得分:0)

坦率地说,我相信人们会使用这些依赖注入库/框架,因为他们只知道如何在运行时执行操作,而不是加载时间。通过设置您的CLASSPATH环境变量(或其他等效语言,例如PYTHONPATHLD_LIBRARY_PATH)来指向您的替代实现(都具有相同的名称)。因此,在接受的答案中,您只需将代码保留为

var logger = new Logger() //sane, simple code

并且将实例化适当的记录器,因为JVM(或您拥有的任何其他运行时或.so加载器)将从通过上述环境变量配置的类中获取它。

不需要将所有东西都变成一个接口,不需要疯狂地产生破碎的对象以将东西注入其中,也不需要具有疯狂的构造函数,并且每一个内部机器都可以暴露在世间。只需使用您正在使用的任何语言的本机功能,而不要提出在任何其他项目中都无法使用的方言。

P.S .:对于测试/模拟也是如此。您可以很好地将环境设置为在加载时间内加载适当的模拟类,并跳过模拟框架的疯狂。