了解依赖注入


109

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

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

对我来说,这只是在争论。我一定想念小姐的意思吗?也许随着更大的项目变得更加明显?

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

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

...

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

DI将是

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

...

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

我确定我在这里糊涂了,有人可以说些什么吗?


5
尝试使用MyInterface和MyClass中的5个,并让多个类形成依赖关系链。设置所有东西成为屁股。
欣快感2014年

159
对我来说,这只是一个争论。我一定是误解了这一点吗? ”不。你说对了。那就是“依赖注入”。现在,您可以为简单的概念想出其他疯狂的术语,这很有趣!
埃里克·利珀特

8
我不能完全支持利珀特先生的评论。我也发现这对我自然而然地做的事情感到困惑。
Alex Humphrey 2014年

16
@EricLippert:虽然正确,但我认为这有点简化。对于习惯了传递数据的普通命令式/ OO程序员而言,Parameters-as-DI并不是一个直接的概念。DI在这里让您传递行为,这需要比普通的程序员更灵活的世界观。
Phoshi 2014年

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

Answers:


106

是的,您可以通过构造函数或属性来注入依赖项。
原因之一是不要让MyClass陷入如何构造MyInterface实例的细节中。MyInterface本身可能具有完整的依赖关系列表,如果您实例化了MyClass内部的所有MyInterface依赖关系,则MyClass的代码将变得非常丑陋。

另一个原因是进行测试。
如果您对文件读取器接口有依赖性,并且通过诸如ConsumerClass之类的构造函数注入了此依赖性,这意味着在测试期间,您可以将文件读取器的内存实现传递给ConsumerClass,而无需在测试期间进行I / O。


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

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

1
@MattGibson好点。
Stefan Billiet 2014年

2
+1用于测试,结构合理的DI将有助于加快测试速度和进行包含在要测试的类中的单元测试。
UmurKontacı2014年

52

如何实现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();
        }
    }

剩下的问题是,我们需要DependencyManager为每个配置编写一个新的子类,并且可以管理的依赖项的数量受到相当的限制(每个新的依赖项在接口中都需要一个新的方法)。

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

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);

在Java之类的语言中,我可以使依赖项管理器的行为类似于Map<Class, Object>

Bar bar = dm.make(Bar.class);

Bar.class可以在运行时配置解析到哪个实际类,例如,通过维护Map<Class, Class>将接口映射到实现的类。

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

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

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

class Foo {
    @Inject(Bar.class)
    private Bar bar;

    @Inject(Qux.class)
    private Qux qux;

    ...
}

dm.make(Foo.class);  // takes care of initializing "bar" and "qux"

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


7
这是个很棒的例子,尽管我需要花一些时间阅读它来消化所有内容,但是感谢您花这么多时间。
MyDaftQuestions 2014年

33
每次简化都会变得更加有趣,这很有趣
Kromster 2014年

48

我们在Stack Overflow上的朋友对此很好的回答。我最喜欢的是第二个答案,包括引号:

“依赖注入”是5美分概念的25美元术语。(...)依赖注入意味着给对象一个实例变量。(...)。

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


10
我读过这篇文章……直到现在,这还是没有道理的。依赖实际上并不是一个可以理解的概念(无论它有多强大),但它确实有一个大名,并且与之相关的互联网干扰很大!
MyDaftQuestions 2014年

18

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

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.

当然,还有更多的优势,但是这些是将创意卖给我的。


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

2
我对您参考的书也很感兴趣,但令我震惊的是,有一本584页的书与此主题有关。
史蒂夫

4

依赖注入只是传递一个参数,但这仍然会导致一些问题,该名称的目的是将重点放在为什么创建对象与传递对象之间的区别很重要的原因上。

这很重要,因为(很明显)在对象A调用类型B的任何时间,A都取决于B的工作方式。这意味着,如果A决定要使用哪种具体的B类型,那么在不修改A的情况下,外部类如何使用A就会失去很多灵活性。

我之所以这么说是因为您谈论的是依赖注入的“遗漏”,而这正是人们喜欢这样做的大部分原因。这可能只是意味着传递参数,但是是否这样做很重要。

此外,在大型项目中,实际实施DI的一些困难确实表现得更好。通常,您将把要使用的具体类的实际决定一直移到顶部,因此您的start方法可能类似于:

public void Start()
{
    MySubSubInterfaceA mySubSubInterfaceA = new mySubSubInterfaceA();
    MySubSubInterfaceB mySubSubInterfaceB = new mySubSubInterfaceB();     
    MySubInterface mySubInterface = new MySubInterface(mySubSubInterfaceA,mySubSubInterfaceB);
    MyInterface myInterface = new MyInterface(MySubInterface);
    MyClass class = new MyClass(myInterface);
}

但是还有另外400行这种铆接代码 如果您想随着时间的流逝保持这种状态,那么DI容器的吸引力就会更加明显。

另外,请想象一个实现IDisposable的类。如果使用它的类通过注入而不是自己创建它,那么他们如何知道何时调用Dispose()?(这是DI容器处理的另一个问题。)

因此,可以肯定的是,依赖注入只是传递一个参数,但是实际上,当人们说依赖注入时,它们的意思是“将参数传递给您的对象,而不是让对象自己实例化某些东西的方法”,并处理了缺陷的所有好处。这种方法可能需要借助DI容器。


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

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

4

在课堂上,这很容易。

“依赖注入”简单地回答了“我将如何找到我的合作者”这个问题,并“将他们推向了您-您不必自己去得到他们”。(它类似于-但与-“控制反转”不同,在该问题中,“我将如何根据输入对我的操作进行排序?”的问题与此类似)。

让您的协作者为您服务的唯一好处是,使客户端代码能够使用您的类来构成一个适合其当前需求的对象图……您没有通过私下决定来任意确定该图的形状和可变性合作者的具体类型和生命周期。

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

值得注意的是,如果您避免实例化自己的协作者,则您的类因此必须从构造函数,属性或方法参数中获取其协作者(顺便说一句,后一种选择通常被忽略了……对于一个班级的合作者来说,成为其“状态”的一部分并不总是很有意义)。

那是一件好事。

在应用程序级别...

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

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

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

...可能是重要的设计力量。

在这种最极端的情况下,您可以选择采用一种非常通用的方法,并为最终用户提供一种通用的机制,以“连接” 他们选择的对象图,甚至允许他们为该接口提供具体的实现。运行!(您的文档是一颗闪闪发光的宝石,您的用户非常聪明,至少熟悉您的应用程序对象图的粗略轮廓,但切勿碰巧拥有编译器)。理论上,这种情况可以在某些“企业”情况下发生。

在那种情况下,您可能有一种声明性语言,它允许您的用户表达任何类型,这种类型的对象图的组成以及神话般的最终用户可以混合和匹配的接口选项板。为了减少用户的认知负担,您更喜欢“按惯例配置”的方法,这样他们只需要介入并覆盖感兴趣的对象图片段,而不必费力地处理整个事情。

你这可怜的草皮!

因为您不愿意自己编写所有内容(但是认真地,请检查您的语言的YAML绑定),所以您正在使用某种类型的DI框架。

根据该框架的成熟度,即使有意义,您也可能无法选择使用构造函数注入(协作者在对象的整个生命周期中都没有改变),从而迫使您使用Setter Injection(即使协作者不会在对象的整个生命周期内发生变化,即使在没有逻辑上的理由为什么接口的所有具体实现都必须具有特定类型的协作者的情况下也是如此。如果是这样,尽管您在整个代码库中都勤奋地使用了“已使用的接口”,但您当前正处在强大的耦合中-恐怖!

但是,希望您使用了一个DI框架,该框架为您提供了构造函数注入的选项,并且您的用户对您有点脾气暴躁,因为他们没有花更多时间考虑他们需要配置的特定事物,并为他们提供了一个更适合任务的UI在眼前。(尽管公平地说,您可能确实尝试过想办法,但是JavaEE让您失望了,您不得不求助于这种可怕的黑客手段)。

开机提示

您从未使用过Google Guice,它为编码人员提供了一种无需编写代码即可通过代码编写对象图的方法。啊!


0

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

对于简单明了的示例,您实际上并不需要“依赖注入”。您可以通过传递给构造函数的简单参数来完成此操作,正如您所指出的那样。

但是,我认为在讨论应用程序级资源时,专用的“依赖关系注入”机制非常有用,在这种情况下,调用者和被调用者都不知道这些资源(也不希望知道!)。

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

而且,一般而言,每个单例都是DI的良好候选者。您拥有和使用的它们越多,您需要DI的机会就越多。

对于这些类型的资源,很显然,通过参数列表将它们四处移动是没有意义的,因此发明了DI。

最后要注意的是,您还需要一个DI容器,它将进行实际的注入...(同样,从实现细节中释放调用方和被调用方)。


-2

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


-4

除了amon的答案,这是另一种选择

使用构建器:

Foo类{
    私人酒吧
    私人Qux qux;

    私人Foo(){
    }

    公共Foo(构建器生成器){
        this.bar = builder.bar;
        this.qux = builder.qux;
    }

    公共静态类生成器{
        私人酒吧
        私人Qux qux;
        公共Buider setBar(酒吧){
            this.bar = bar;
            返回这个
        }
        公共生成器setQux(Qux qux){
            this.qux = qux;
            返回这个
        }
        公共Foo build(){
            返回新的Foo(this);
        }
    }
}

//在生产中:
Foo foo =新的Foo.Builder()
    .setBar(新的Bar())
    .setQux(新Qux())
    。建立();

//在测试中:
Foo testFoo =新的Foo.Builder()
    .setBar(新的BarMock())
    .setQux(新Qux())
    。建立();

这就是我用来依赖的东西。我不使用任何DI框架。他们挡住了路。


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

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

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

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

构造函数DI正在传递参数。除了要传递接口对象而不是具体的类对象。正是这种“依赖”取决于通过DI解决的具体实现。构造函数不知道它正在传递的类型,也不在乎,它只是知道它正在接收实现接口的类型。我建议PluralSight,它有一个很棒的初学者DI / IOC课程。
Hardgraf 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.