我正在阅读有关依赖项注入(DI)的信息。对我来说,这是一件非常复杂的事情,因为我在阅读它的同时也提到了控制反转(IoC),因此我感到自己将要踏上旅途。

我的理解是:不要在也使用它的类中创建模型,而是将模型(已经填充了有趣的属性)传递(注入)到需要它的地方(到新的类,该类可以将它作为构造函数)。

对我来说,这只是传递一个论点。我一定想念小姐的意思吗?

我的理解是非DI(使用伪代码):



public void Start()
{
    MyClass class = new MyClass();
}

...

public MyClass()
{
    this.MyInterface = new MyInterface(); 
}


我确定有人在糊涂中吗?

评论

尝试使用MyInterface和MyClass中的5个,并使多个类形成依赖关系链。设置所有东西成为屁股上的痛苦。

“对我来说,这只是一个争论。我一定是误解了这一点吗?”不。你说对了。那就是“依赖注入”。现在看看您还能为简单的概念想出其他疯狂的术语,这很有趣!

我不能完全支持利珀特先生的评论。我也发现它对我本来自然要做的事情感到困惑。
@EricLippert:虽然正确,但我认为这有点简化。对于习惯了传递数据的一般命令/ OO程序员而言,“将参数作为DI”并不是一个直接的概念。 DI可以让您传递行为,这需要比普通程序员更灵活的世界观。

@Phoshi:你说的很对,但是你也要指出为什么我怀疑“依赖注入”是一个好主意。如果我编写的类的正确性和性能取决于另一个类的行为,那么我要做的最后一件事就是让该类的用户负责正确地构造和配置依赖项!那是我的工作。每次让使用者使用依赖项时,都会创建一个点,表示使用者可能做错了。

#1 楼

好吧,是的,您可以通过构造函数或属性来注入依赖项。
原因之一是不要让MyClass陷入如何构造MyInterface实例的细节中。 MyInterface本身可能具有完整的依赖关系列表,如果您实例化了MyClass内部的所有MyInterface依赖关系,MyClass的代码将变得非常丑陋。
如果您对文件阅读器界面有依赖性,并且通过诸如ConsumerClass之类的构造函数注入了这种依赖性,则意味着在测试期间,您可以将文件阅读器的内存实现传递给ConsumerClass,避免在测试过程中进行I / O。

评论


这很好,很好地解释了。请接受一个虚构的+1,因为我的代表还不允许这样做!

– MyDaftQuestions
2014年3月13日10:00

复杂的构造方案很适合使用Factory模式。这样,工厂就承担了构造对象的责任,这样,类本身就不必这样做,并且您坚持单一责任原则。

–马特·吉布森(Matt Gibson)
2014年3月13日在12:41

@MattGibson好点。

– Stefan Billiet
2014年3月13日13:30在

+1用于测试,结构合理的DI将有助于加快测试速度,并使您要测试的类中包含单元测试。

– UmurKontacı
2014年4月5日19:00

#2 楼

如何实现DI很大程度上取决于所使用的语言。这里是一个简单的非DI示例:

 class Foo {
    private Bar bar;
    private Qux qux;

    public Foo() {
        bar = new Bar();
        qux = new Qux();
    }
}
 


很烂,例如当进行测试时,我想对bar使用模拟对象。因此,我们可以使其更加灵活,并允许通过构造函数传递实例:

 class Foo {
    private Bar bar;
    private Qux qux;

    public Foo(Bar bar, Qux qux) {
        this.bar = bar;
        this.qux = qux;
    }
}

// in production:
new Foo(new Bar(), new Qux());
// in test:
new Foo(new BarMock(), new Qux());
 


这已经是依赖注入的最简单形式。但这仍然很糟糕,因为一切都必须手动完成(而且,因为调用者可以持有对我们内部对象的引用,从而使我们的状态无效)。



一个选项是由抽象工厂生成Foo




另一个选择是将工厂传递给构造函数:

 interface FooFactory {
    public Foo makeFoo();
}

class ProductionFooFactory implements FooFactory {
    public Foo makeFoo() { return new Foo(new Bar(), new Baz()) }
}

class TestFooFactory implements FooFactory {
    public Foo makeFoo() { return new Foo(new BarMock(), new Baz()) }
}

FooFactory fac = ...; // depends on test or production
Foo foo = fac.makeFoo();
 



与此相关的其余问题是,我们需要为每个配置编写一个新的interface DependencyManager { public Bar makeBar(); public Qux makeQux(); } class ProductionDM implements DependencyManager { public Bar makeBar() { return new Bar() } public Qux makeQux() { return new Qux() } } class TestDM implements DependencyManager { public Bar makeBar() { return new BarMock() } public Qux makeQux() { return new Qux() } } class Foo { private Bar bar; private Qux qux; public Foo(DependencyManager dm) { bar = dm.makeBar(); qux = dm.makeQux(); } } 子类,并且可以管理的依赖项数量受到相当大的限制(每个新的依赖关系需要在接口中使用新方法。)

借助反射和动态类加载等功能,我们可以规避此问题。但这在很大程度上取决于所使用的语言。在Perl中,可以通过类的名称来引用类,而我可以这样做

 DependencyManager 


在Java之类的语言中,我可以使依赖项管理器的行为类似于package Foo { use signatures; sub new($class, $dm) { return bless { bar => $dm->{bar}->new, qux => $dm->{qux}->new, } => $class; } } my $prod = { bar => 'My::Bar', qux => 'My::Qux' }; my $test = { bar => 'BarMock', qux => 'QuxMock' }; $test->{bar} = 'OtherBarMock'; # change conf at runtime my $foo = Foo->new(rand > 0.5 ? $prod : $test);

 Map<Class, Object> 


可以在运行时配置将Bar bar = dm.make(Bar.class); 解析为哪个实际类,例如通过维护将接口映射到实现的Bar.class

 Map<Class, Class> 


编写构造函数时仍涉及一个手动元素。但是我们可以使构造函数完全不必要,例如通过注释驱动DI:

 Map<Class, Class> dependencies = ...;

public <T> T make(Class<T> c) throws ... {
    // plus a lot more error checking...
    return dependencies.get(c).newInstance();
}
 


下面是一个微小的(且非常受限制)DI框架:http://ideone.com/b2ubuF,尽管此实现对于不可变对象完全不可用(此朴素的实现不能为构造函数使用任何参数)。

评论


这是个很棒的例子,虽然花了我几次阅读来消化所有内容,但还是感谢您花这么多时间。

– MyDaftQuestions
2014年3月13日在13:32

每次简化都变得更加复杂很有趣

– Kromster
2014年3月14日下午5:18

#3 楼

我们在Stack Overflow上的朋友对此有一个很好的答案。我最喜欢的是第二个答案,包括引号:“ Dependency Injection”(依赖注入)是5美分概念的25美元术语。 (...)依赖注入意味着给对象一个实例变量。 (...)。


,来自James Shore的博客。当然,在此之上还有更复杂的版本/模式,但这足以基本了解正在发生的事情。它们不是从对象创建自己的实例变量,而是从外部传入。

评论


我读过这篇文章……直到现在,这还是没有意义的。依赖性实际上并不是要理解的概念那么大(不管它有多强大),但是它确实有一个很大的名字,并且与之相关的互联网噪音很大!

– MyDaftQuestions
14年3月13日在13:54

#4 楼

假设您正在客厅中进行室内设计:您在天花板上安装了花式吊灯,并插入了优雅的配套落地灯。一年后,你可爱的妻子决定让她少一点正式的房间。哪种照明灯具更容易更换?

DI背后的想法是在各处使用“插入式”方法。如果您生活在没有石膏板且所有电线都暴露在外的简单棚屋中,这似乎并不重要。 (您是一个杂物工-您可以进行任何更改!)同样,在小型简单应用程序上,DI可能会增加比其价值更大的复杂性。

但是对于大型而复杂的应用,DI对于长期维护是必不可少的。它允许您从代码(例如数据库后端)中“拔出”整个模块,并与完成相同任务但以完全不同的方式完成操作的不同模块(例如,云存储系统)交换它们。

顺便说一句,如果没有Mark Seeman建议的.NET中的依赖注入建议,对DI的讨论将是不完整的。如果您熟悉.NET,并且打算参与大规模的SW开发,那么这是必读的。他对这个概念的解释远比我能解释的要好。

让我留下您的代码示例忽略的DI的最后一个基本特征。 DI使您可以利用格言“程序到接口”中封装的巨大的灵活性潜力。稍微修改一下示例:

public void Main()
{
    ILightFixture fixture = new ClassyChandelier();
    MyRoom room = new MyRoom (fixture);
}
...
public MyRoom(ILightFixture fixture)
{
    this.MyLightFixture = fixture ; 
}


但是现在,因为MyRoom是为ILightFixture界面设计的,所以我可以轻松进入明年并在Main函数中更改一行(“注入”不同的“依赖项”),然后立即在MyRoom中获得新功能(无需重建或重新部署MyRoom(如果它恰好位于单独的库中),当然,这要假定您所有的ILightFixture实现都考虑了Liskov Substitution的设计。所有,只需一个简单的更改即可:

public void Main()
{
    ILightFixture fixture = new MuchLessFormalLightFixture();
    MyRoom room = new MyRoom (fixture);
}


另外,我现在可以进行单元测试MyRoom(您是单元测试,对吗?)并使用“模拟”灯具,这使我可以完全独立于MyRoom来测试ClassyChandelier.

当然还有更多的优点,但这是将其售予我的。

评论


我想说谢谢,这对我很有帮助,但是他们不希望您这样做,而是使用注释来建议改进,因此,如果您愿意,可以添加提到的其他优点之一。否则,谢谢,这对我很有帮助。

–史蒂夫
18年1月19日在15:12

我对您参考的书也很感兴趣,但令我震惊的是,有关该主题的书共584页。

–史蒂夫
18年1月19日在15:19

#5 楼

依赖注入只是传递一个参数,但这仍然会导致一些问题,该名称的目的是将重点放在为什么创建一个对象与传递一个对象之间的区别很重要的原因上。很重要,因为(很明显)任何时候对象A调用类型B时,A取决于B的工作方式。这意味着,如果A决定要使用哪种具体的B类型,那么在不修改A的情况下,外部类如何使用A就会失去很多灵活性。

我之所以这么说是因为您谈论的是依赖注入的“遗漏”,而这正是人们喜欢这样做的大部分原因。这可能仅意味着传递参数,但是是否这样做很重要。通常,您将把使用哪种具体类的实际决定一直移到顶部,因此您的启动方法可能类似于:

这类铆接代码行。如果您想随着时间的流逝保持这种状态,那么DI容器的吸引力就会越来越明显。

还要想象一个实现IDisposable的类。如果使用它的类通过注入而不是自己创建它,那么他们如何知道何时调用Dispose()? (这是DI容器要处理的另一个问题。)因此,可以肯定的是,依赖注入只是传递一个参数,但实际上,当人们说依赖注入时,它们的意思是“将参数传递给对象vs “让您的对象自己实例化某些东西的替代方法”,并可能借助DI容器来解决这种方法的缺点的所有好处。

评论


大量的子和接口!您是要新建一个接口,而不是实现该接口的类吗?似乎不对。

– JBR威尔金森
2014年3月13日23:31



@JBRWilkinson我的意思是一个实现接口的类。我应该说清楚一点。但是,重点是大型应用程序将具有带有依赖关系的类,这些依赖关系又具有依赖关系,而依赖关系又具有依赖关系...在Main方法中手动进行设置是大量构造函数注入的结果。

–psr
14年3月14日在0:04

#6 楼

在班级,这很容易。

“依赖注入”简单地回答了“我将如何找到我的合作者”这个问题,并带有“他们被逼着你-你不必走了就走他们自己”。 (这与“控制反转”类似,但不相同,在该问题中,“我将如何根据输入对我的操作进行排序?”的问题与此类似)。

唯一的好处是让您的协作者来找您是使客户代码可以使用您的类来构成一个适合其当前需求的对象图...您尚未通过私下确定具体类型和类型来任意确定图的形状和可变性您的协作者的生命周期。

(所有其他好处,例如可测试性,松耦合等等,很大程度上取决于接口的使用,而与依赖注入的关系不大,尽管DI确实促进了接口的使用。)

值得注意的是,如果您避免实例化自己的协作者,则您的类因此必须从构造函数,属性或方法中获取其协作者。 -argument(顺便说一句,后一种选择经常被忽略……它并不总是使可以让一个类的协作者成为其“状态”的一部分)。

这是一件好事。

在应用程序级别...

对于每个类的事物而言,就这么多。假设您有一堆遵循“不要实例化自己的协作者”规则的类,并希望从中进行应用。最简单的方法是使用良好的旧代码(这是调用构造函数,属性和方法的一种非常有用的工具!)组成所需的对象图,并向其添加一些输入。 (是的,图中的某些对象本身将是对象工厂,它们已作为协作者传递给图中的其他长期存在的对象,可供使用...您无法预先构造每个对象! )。

...您需要“灵活地”配置应用程序的对象图...

根据您的其他(与代码无关的)目标,您可能希望给最终用户一些控制权这样部署的对象图。无论是属于您自己设计的带有某些名称/值对的文本文件,XML文件,自定义DSL,声明性图形描述语言(例如, YAML,一种命令式脚本语言,例如JavaScript,或其他适合于当前任务的语言。满足用户需求的方式来构成有效的对象图所需的一切。

...可能是巨大的设计力量。

在这种极端情况下,您可以选择采用一种非常通用的方法,并为最终用户提供一种通用的机制来“连接”他们选择的对象图,甚至允许他们为运行时提供接口的具体实现! (您的文档是一颗闪闪发光的宝石,您的用户非常聪明,至少熟悉您的应用程序对象图的粗略轮廓,但碰巧没有编译器可用)。从理论上讲,这种情况可能发生在某些“企业”情况下。神话般的最终用户可以混合和匹配的接口调色板。为了减少用户的认知负担,您更喜欢采用“按惯例配置”的方法,这样,他们只需介入并覆盖感兴趣的对象图片段,而无需费力地处理整个事情。

你的草皮很差!某种框架。

根据该框架的成熟度,即使有意义,您也可能无法选择使用构造函数注入(协作者在对象的整个生命周期中都没有改变),从而迫使您使用Setter Injection(即使协作者不会在对象的整个生命周期内发生变化,即使在没有逻辑上的理由为什么接口的所有具体实现都必须具有特定类型的协作者的情况下也是如此。如果是这样,尽管您在整个代码库中都勤奋地使用了“已使用的接口”,但您目前正处在强大的耦合中! ,而您的用户则对您有点脾气暴躁,因为他们没有花更多时间考虑他们需要配置的特定事物,而是为他们提供了更适合手头任务的UI。 (虽然公平地说,您可能确实尝试过想办法,但是JavaEE让您失望了,您不得不求助于这种可怕的黑客手段。)您从未使用过Google Guice,它为编码人员提供了一种无需编写代码即可通过代码编写对象图的方法。啊!

#7 楼

很多人在这里说DI是一种将实例变量的初始化外包的机制。

对于显而易见的简单示例,您实际上并不需要“依赖注入”。您可以通过传递给构造函数的简单参数来完成此操作,正如您所提到的那样。其中调用者和被调用者都不知道这些资源(也不希望知道!)。

例如:数据库连接,安全上下文,日志记录工具,配置文件等。

此外,总的来说,每个单身人士都是DI的理想人选。您拥有和使用的它们越多,您需要DI的机会就越多。

对于这些资源,很明显,通过参数列表来移动它们是没有意义的,因此DI是发明的。还需要一个DI容器,它将进行实际的注入...(同样,从实现细节中释放调用方和被调用方)。

#8 楼

如果传入抽象引用类型,则调用代码可以传入扩展/实现抽象类型的任何对象。因此,将来使用该对象的类可以使用已创建的新对象,而无需修改其代码。

#9 楼

除了amon的答案,这是另一个选择。使用构建器:

我不使用任何DI框架。他们妨碍了。

评论


这与一年前的其他答案并没有多大关系,也无法解释为什么构建器可以帮助请求者理解依赖项注入。

–user22815
2015年3月4日在21:09



@Snowman这是DI的另一个选项。对不起,您没有那样看。感谢您的不赞成投票。

–user3155341
2015年3月4日22:00



询问者想了解DI是否真的像传递参数一样简单,他没有要求替代DI。

–user22815
2015年3月4日在22:23

OP的示例非常好。您的示例为简单的想法提供了更多的复杂性。他们没有说明问题的问题。

–亚当·扎克曼(Adam Zuckerman)
2015年3月4日23:32

构造函数DI正在传递参数。除了要传递接口对象而不是具体的类对象。正是这种依赖于通过DI解决的具体实现。构造函数不知道它正在传递的类型,也不在乎,它只知道它正在接收实现接口的类型。我建议使用PluralSight,它具有出色的初学者DI / IOC课程。

– Hardgraf
2015年6月8日14:22