为什么使用依赖注入?

516 dependency-injection

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

我不太明白它解决了什么问题.它看起来像是一种说法:"嗨.当你遇到这个函数时,返回一个这种类型的对象并使用这些参数/数据."
但是......为什么我会用它呢?注意我从来不需要使用object,但我明白这是什么.

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

Gol*_*den 829

首先,我想解释一下我为这个答案做出的假设.它并不总是如此,但经常是:

界面是形容词; 课程是名词.

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

因此,例如,一个接口可以是这样的东西IDisposable,IEnumerableIPrintable.类是这些接口中的一个或多个的实际实现:List或者Map两者都可以是实现IEnumerable.

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

到现在为止还挺好.

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

var logger = new Logger();
Run Code Online (Sandbox Code Playgroud)

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

当然,你不要想改变所有的代码(同时你拥有它gazillions)和替换所有行

var logger = new Logger();
Run Code Online (Sandbox Code Playgroud)

通过:

var logger = new TcpLogger();
Run Code Online (Sandbox Code Playgroud)

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

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

ICanLog logger = new Logger();
Run Code Online (Sandbox Code Playgroud)

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

ICanLog logger = LoggerFactory.Create();
Run Code Online (Sandbox Code Playgroud)

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

当然,现在您可以概括这个工厂,并使其适用于任何类型:

ICanLog logger = TypeFactory.Create<ICanLog>();
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

依赖注入现在是这一行的下一步:只需要去掉服务定位器的这个单一依赖:代替各种类询问服务定位器是否有特定接口的实现,你再一次 - 恢复对谁实例化什么的控制.

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

public Database(ICanLog logger) { ... }
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

但基本上就是这样.

希望有所帮助.

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

  • 这是我从未得到的关于DI的事情:它使得架构**变得更加复杂.然而,正如我所看到的,使用非常有限.这些示例当然总是相同的:可互换的记录器,可互换的模型/数据访问.有时可互换的视图.但就是这样.这几个案例真的证明了一个复杂得多的软件架构吗? - 完全披露:我已经使用了DI效果很好,但这是一个非常特殊的插件架构,我不会概括. (131认同)
  • 我称之为ICanLog,因为我们经常使用没有任何意义的单词(名词).例如,什么是经纪人?经理?即使是存储库也没有以独特的方式定义.将所有这些作为名词的东西都是OO语言的典型疾病(参见http://steve-yegge.blogspot.de/2006/03/execution-in-kingdom-of-nouns.html).我想表达的是,我有一个可以为我做日志记录的组件 - 所以为什么不这样称呼呢?当然,这也是我作为第一人称,因此ICanLog(ForYou). (26认同)
  • @David单元测试工作得很好 - 毕竟,*unit*独立于其他东西(否则它不是一个单位).没有DI容器的*不工作是模拟测试.很公平,我不相信模拟的好处超过了在所有情况下添加DI容器的额外复杂性.我做严格的单元测试.我很少做嘲笑. (18认同)
  • @GoloRoden,你为什么要调用接口ICanLog而不是ILogger?我和另一个经常这样做的程序员一起工作,我永远无法理解这个惯例?对我来说,就像调用IEnumerable ICanEnumerate一样? (17认同)
  • 当然你也可以这样做,但是你必须在每一个类中实现这个逻辑,它将为实现的可交换性提供支持.这意味着许多重复的冗余代码,这也意味着一旦您决定现在需要它,您需要触摸现有的类并部分重写它.DI允许您在任意类上使用它,您不必以特殊方式编写它们(除了将依赖项定义为构造函数中的参数). (7认同)
  • @ acidzombie24"当我听到人们使用DI -a很多时,这让我感到困扰".最让我困扰的是,你得到了所有这些复杂性****没有理由,因为实际上,没有任何东西被注入,而且没有任何测试_.我讨厌这个,因为它是无偿的复杂性.我说YAGNI/KISS (7认同)
  • @KonradRudolph我总是发现,只要我想在依赖于其他系统的类上单元测试方法就需要注入依赖项.是模拟还是仅仅存储依赖关系.您是否可以参考单元测试的描述,其中不需要替换依赖项?我对其他方法很感兴趣. (5认同)
  • @ acidzombie24:简而言之 - 依赖注入是解决依赖关系的一种(不是*THE*)方式.你没有幸福,那就没有必要使用它了.但如果你遇到由未解决的依赖关系引起的问题(可能是在测试中或其他什么),我建议你选择DI,因为这是一个广泛传播的最佳实践.无需弄清楚自己的风格;-). (5认同)
  • @KonradRudolph就使用IOC而言,我和你在一起,对于许多应用程序而言,这是一种过度杀伤力,但是我很感兴趣你如何建议单元测试而不设计类来允许注入依赖项?经常注入基础设施,但通常也是您希望能够替换以进行测试的域代码.注意这些依赖关系作为ctor params从来没有让我感到更复杂.当然,你提到想要可插拔行为的情况使得DI成为必须.我已经很长一段时间发现SOL中的D是OO模式的很多功能的关键推动因素. (4认同)
  • @ acidzombie24我从不使用单身人士.这首先是DI的巨大好处,它否定了对服务定位器等模式的需求.当然我认为我的代码设计得很好 - 这就是我设计它的原因:)对我个人而言,理解类的最好方法是运行它的方法,看看会发生什么.我发现设计的代码允许DI使得大型代码库的运行部分更容易隔离 - 对于单元测试,它恰好是以可重复的方式执行此操作的便捷方式. (3认同)
  • 我本来会做的事情就是保留一切(如不要在任何地方更改`new Logger()`或在任何地方写`LoggerFactory.Create();。我都会对其进行更改,以便Logger具有已设置的ICanLog成员)通过读取XML文件并将所有命令转发给构造函数,无需进行任何代码更改,只需将原始Logger类重命名为SomeConcreteLogger,然后将转发类创建为Logger。 (2认同)
  • 我不同意DI极大地使架构复杂化,但是确实更加复杂(假设您将DI容器用作实现DI的黑匣子,而不是自己动手做)。DI的好处很多,但它鼓励进行单元测试,TDD和模块化。但是,如果您滚动自己的DI容器,则可能不值得其增加的复杂性。 (2认同)
  • 如何在不使用DI来模拟依赖项的情况下编写单元测试(依赖于其他对象)?或者这是您需要不同架构的地方? (2认同)

Dan*_*den 491

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

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

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();
}
Run Code Online (Sandbox Code Playgroud)

你写这样的代码:

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();
}
Run Code Online (Sandbox Code Playgroud)

就是这样.认真.这给你带来了很多好处.两个重要的功能是能够从中心位置(Main()函数)控制功能,而不是在整个程序中传播它,并且能够更容易地单独测试每个类(因为您可以将模拟或其他伪造的对象传递到其构造函数中一个真正的价值).

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

  • @ user962206,也考虑如果B在其构造函数中需要一些参数会发生什么:为了实例化它,A必须知道那些参数,这些参数可能与A完全无关(它只是想依赖于B) ,而不是B依赖的东西).将已构造的B(或任何B的子类或模拟)传递给A的构造函数解决了这个问题并使A仅依赖于B :) (64认同)
  • @ acidzombie24:像许多设计模式一样,除非你的代码库足够大,简单的方法才能成为一个问题,否则DI并不是很有用.我的直觉是,在您的应用程序拥有超过20,000行代码和/或超过20个其他库或框架的依赖项之前,DI实际上不会是一个改进.如果你的应用程序小于那个,你可能仍然喜欢以DI风格编程,但差异不会那么显着. (17认同)
  • @ user962206考虑如何独立于B测试A. (15认同)
  • 为什么我更喜欢第二个代码而不是第一个代码呢?第一个只有new关键字,这有什么帮助? (7认同)
  • @DanielPryden我不认为代码大小与代码的动态性有关.如果您经常添加适合相同界面的新模块,则不必经常更改相关代码. (2认同)

ces*_*sor 35

正如其他答案所述,依赖注入是一种在使用它的类之外创建依赖项的方法.你从外面注入它们,并从你的班级内部控制他们的创造.这也是依赖注入是控制反转(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));
}
Run Code Online (Sandbox Code Playgroud)

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

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

这里的问题是,这个测试很可能总是失败,因为正在设置的日期将与被声明的日期不同,即使您只是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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

[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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

并且...... 还应该注意,如果一个对象的所有依赖项都可用,那么它只能在运行时正确地实现其目的,因此在设置属性注入时没有多大用处.在我看来,在调用构造函数时应该满足所有依赖项,因此构造函数注入是最佳选择.

我希望有所帮助.

  • 这实际上看起来像一个可怕的解决方案.我肯定会写代码更像[Daniel Pryden回答](http://stackoverflow.com/a/14301816/34537)建议但是对于那个特定的单元测试我只是在函数前后做DateTime.Now并检查是否时间介于两者之间?添加更多接口/更多代码行似乎对我来说是一个糟糕的主意. (4认同)
  • 我不喜欢通用A(B)示例,我从未觉得需要记录器才能拥有100个实现.这是我最近遇到的一个例子,它是解决它的5种方法之一,其中一个实际包含使用PostSharp.它说明了一种基于经典类的ctor注入方法.你能提供一个更好的真实世界的例子,说明你在哪里遇到了很好用的DI吗? (3认同)
  • 我从来没有看到过DI的好用.这就是我写这个问题的原因. (2认同)
  • 我没有发现它有用.我的代码总是很容易做测试.看来DI对于代码错误的大代码库来说是好的. (2认同)
  • 我同意这个答案,因为在我的职业生涯中,我从未见过有人真正利用依赖注入的好处,即,总是只有一个子类实现接口。我在 DI 中看到的唯一有用的好处是在您的单元测试中,因为就像其他人所说的那样,您可以使用 Mockitto 和其他工具来注入类的“测试”版本,以便您可以更轻松地编写具有预期结果的测试。但就像其他人说的那样,我相信你可以在没有 DI 的情况下进行类似的测试。我对 DI 的印象是它使代码复杂化,而且奖励很少 (2认同)

Ita*_*agi 13

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

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

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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();
}
Run Code Online (Sandbox Code Playgroud)

**代码不会编译,我知道:)


Loe*_*man 6

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

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

I_Dosomething x = new Impl_Dosomething();
Run Code Online (Sandbox Code Playgroud)

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

I_Dosomething x = ServiceLocator.returnDoing(String pKey);
Run Code Online (Sandbox Code Playgroud)

DI例子:

I_Dosomething x = DIContainer.returnThat();
Run Code Online (Sandbox Code Playgroud)

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

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

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