通过构造函数或属性设置器进行依赖注入?


151

我正在重构一个类,并为其添加新的依赖关系。该类当前正在构造函数中使用其现有的依赖项。因此,为了保持一致性,我将参数添加到构造函数中。
当然,有一些子类,甚至还有更多用于单元测试的子类,所以现在我正在玩改变所有构造函数以匹配的游戏,并且这需要很长时间。
它使我认为,将属性与setter一起使用是获取依赖项的更好方法。我不认为注入的依赖关系应该是构造类实例的接口的一部分。您添加了一个依赖项,现在所有用户(子类以及直接实例化您的任何人)突然都知道了。感觉就像封装的破坏。

这似乎不是此处现有代码的模式,因此我希望了解什么是一般共识,即构造函数与属性的优缺点。使用属性设置器更好吗?

Answers:


126

这要看情况 :-)。

如果类在没有依赖项的情况下无法完成其工作,则将其添加到构造函数中。该类需要新的依赖关系,因此您希望所做的更改使事情中断。同样,创建未完全初始化的类(“两步构造”)也是一种反模式(IMHO)。

如果该类可以在没有依赖项的情况下工作,则可以使用setter。


11
我认为在很多情况下,最好使用Null Object模式并坚持在构造函数上要求引用。这避免了所有空检查和增加的循环复杂性。
Mark Lindell 2010年

3
@Mark:好点。但是,问题是关于向现有类添加依赖项。然后保留无参数构造函数可实现向后兼容性。
sleske 2010年

什么时候需要依赖项才能起作用,但是通常只要注入该依赖项即可。那么,该依赖关系应该可以通过属性或构造函数重载“覆盖”吗?
Patrick Szalapski 2010年

@帕特里克:“类无法完成其依赖项”,我的意思是没有合理的默认值(例如,类需要数据库连接)。在您的情况下,两者都会起作用。我通常还是选择构造函数的方法,因为它降低了复杂性(例如,两次调用setter会怎样?)?
sleske 2010年


21

类的用户应该知道给定类的依赖关系。如果我有一个类,例如,一个连接到数据库的类,并且没有提供注入持久层依赖的方法,那么用户将永远不会知道必须要有一个与数据库的连接。但是,如果我更改了构造函数,则会让用户知道持久层存在依赖性。

另外,为避免自己不得不改变对旧构造函数的每次使用,只需将构造函数链接作为新旧构造函数之间的临时桥梁即可。

public class ClassExample
{
    public ClassExample(IDependencyOne dependencyOne, IDependencyTwo dependencyTwo)
        : this (dependnecyOne, dependencyTwo, new DependnecyThreeConcreteImpl())
    { }

    public ClassExample(IDependencyOne dependencyOne, IDependencyTwo dependencyTwo, IDependencyThree dependencyThree)
    {
        // Set the properties here.
    }
}

依赖注入的要点之一就是揭示类具有什么依赖。如果类具有过多的依赖关系,那么可能是时候进行一些重构了:类的每个方法都使用所有依赖关系吗?如果不是这样,那么这是查看可以在哪里拆分类的一个很好的起点。


当然,构造函数链接仅在新参数具有合理的默认值时才有效。但是,否则您还是无法避免破坏事情……
sleske

通常,您将使用依赖项注入之前在方法中使用的任何内容作为默认参数。理想情况下,这将使新的构造函数添加为干净的重构,因为类的行为不会改变。
蓝色医生

1
我就资源管理依赖关系(例如数据库连接)发表观点。我认为我的问题是我要添加依赖项的类具有多个子类。在由容器设置属性的IOC容器世界中,使用setter至少可以减轻所有子类之间构造函数接口重复的压力。
Niall Connaughton

17

当然,使用构造函数意味着您可以一次验证所有内容。如果将事物分配给只读字段,那么从构造开始就可以保证对象的依赖性。

添加新的依赖关系确实很痛苦,但是至少以这种方式,编译器一直抱怨直到正确为止。我认为这是一件好事。


为此加一。此外,这极大地减少了循环依赖的危险...
gilgamash

11

如果您有大量的可选依赖项(已经是一种气味),那么setter注入可能是可行的方法。构造函数注入可以更好地揭示您的依赖关系。


11

通常首选的方法是尽可能使用构造函数注入。

构造函数注入准确地说明了对象正常运行所需的依赖项-没有什么比新建对象和在调用该对象的方法时使它崩溃(因为未设置某些依赖项)更令人讨厌的了。构造函数返回的对象应处于工作状态。

尝试只有一个构造函数,这样可以简化设计并避免歧义(如果不是对于人类,对于DI容器)。

当Mark Seemann 在他的书“ .NET中的依赖项注入”中有了本地默认值时,就可以使用属性注入:依赖项是可选的,因为您可以提供良好的工作实现,但希望允许调用方指定其他不同的实现。需要。

(下面是以前的答案)


我认为,如果强制性注入,则构造函数注入会更好。如果这增加了太多的构造函数,请考虑使用工厂而不是构造函数。

如果注射是可选的,或者您想在中途更改它,则setter注射是不错的选择。我通常不喜欢二传手,但这是一个品味问题。


我认为通常在中途更改注入是不好的样式(因为您正在向对象添加隐藏状态)。但是没有规则当然没有例外...
sleske

是的,那就是为什么我说我不太喜欢setter的原因……我喜欢构造方法,因为它不能更改。
菲利普

“如果这增加了太多的构造函数,请考虑使用工厂而不是构造函数。” 您基本上会推迟所有运行时异常,甚至可能会出错,并最终陷入服务定位器实现的困境。
MeTitus

@Marco这是我以前的回答,您是对的。如果构造函数很多,我会认为该类做了太多的事情:-)或考虑一个抽象工厂。
菲利普(Philippe)2016年

7

这很大程度上取决于个人喜好。就我个人而言,我倾向于使用setter注入,因为我相信它可以在运行时替代实现的方式为您提供更大的灵活性。而且,在我看来,带有很多参数的构造函数并不干净,并且在构造函数中提供的参数应限于非可选参数。

只要类接口(API)清楚执行任务所需的内容,就可以了。


请说明为什么要拒​​绝投票?
nkr1pt

3
是的,带有很多参数的构造函数是不好的。这就是为什么您使用许多构造函数参数来重构类的原因:-)。
sleske

10
@ nkr1pt:大多数人(包括我在内)都同意,setter注入是不好的,如果它允许您创建一个在注入未完成时在运行时失败的类。因此,我相信有人反对您所说的是个人品味。
sleske

7

我个人更喜欢“ 提取并重写 ”“模式”,而不是在构造函数中注入依赖项,这在很大程度上是出于问题中概述的原因。您可以将属性设置为virtual,然后在派生的可测试类中覆盖实现。


1
我认为该模式的正式名称是“模板方法”。
davidjnelson 2012年

6

我更喜欢构造函数注入,因为它有助于“增强”类的依赖关系要求。如果在目录中,则消费者必须设置对象才能编译应用程序。如果您使用setter注入,他们可能直到运行时才知道它们有问题-并且取决于对象,运行时可能会延迟。

当注入的对象本身可能需要大量工作(例如初始化)时,我仍会不时使用setter注入。


6

我进行构造函数注入,因为这似乎是最合逻辑的。就像说我的课需要这些依赖关系来完成其工作一样。如果它是可选的依赖项,那么属性似乎是合理的。

我还使用属性注入来设置容器没有引用的内容,例如使用容器创建的演示者上的ASP.NET视图。

我认为它不会破坏封装。内部工作应保持内部状态,而依赖项应处理不同的问题。


感谢您的回答。当然,构造函数似乎是最受欢迎的答案。但是我确实认为它以某种方式破坏了封装。在进行依赖注入之前,该类将声明并实例化完成其工作所需的任何具体类型。使用DI,子类(和任何手动实例化器)现在知道基类正在使用哪些工具。您添加了一个新的依赖项,现在您必须从所有子类中链接实例,即使它们不需要自己使用依赖项也是如此。
Niall Connaughton,2009年

刚刚写了一个不错的长答案,由于这个网站上的异常而丢失了它!:(总之,基类通常用于重用逻辑。该逻辑可以很容易地进入子类...因此您可以想到基类和子类=一个依赖于多个外部对象的关注点,做不同的工作。事实上,您有依赖关系,并不意味着您需要公开任何以前不公开的内容
David Kiff,2009年

3

一种可能值得考虑的选择是从简单的单个依赖关系中构成复杂的多重依赖关系。也就是说,为复合依赖项定义额外的类。这使事情变得更容易WRT构造函数注入-每个调用的参数更少-同时仍保持必须供应所有依赖关系来实例化事物。

当然,如果存在某种逻辑依赖关系组,这是最有意义的,因此该复合物不仅仅是一个任意集合,并且对于单个复合依赖物有多个依赖物,这是最有意义的-但是参数块“模式”具有已经存在了很长时间,而我见过的大多数对象都是相当武断的。

不过,就我个人而言,我更喜欢使用方法/属性设置器来指定依赖项,选项等。调用名称有助于描述正在发生的事情。不过,最好提供示例“如何设置”片段,并确保相关类进行足够的错误检查。您可能要对安装使用有限状态模型。


3

最近,我遇到了一个类中有多个依赖项的情况,但是每个实现中只有一个依赖项必须更改。由于数据访问和错误日​​志记录的依赖关系可能仅出于测试目的而更改,因此我为这些依赖关系添加了可选参数,并在构造函数代码中提供了这些依赖关系的默认实现。这样,除非类的使用者将其覆盖,否则该类将保持其默认行为。

使用可选参数只能在支持它们的框架中完成,例如.NET 4(适用于C#和VB.NET,尽管V​​B.NET始终具有它们)。当然,您可以通过简单地使用可以由类的使用者重新分配的属性来实现类似的功能,但是您不能通过将私有接口对象分配给构造函数的参数来获得不变性的优势。

综上所述,如果要引入一个必须由每个使用者提供的新依赖关系,则将必须重构构造函数以及使用者类的所有代码。我上面的建议实际上仅在您能够为所有当前代码提供默认实现的情况下适用,而在必要时仍提供覆盖默认实现的功能。



0

这取决于您要如何实现。我更喜欢在不需要经常更改实现值的地方进行构造函数注入。例如:如果Oracle服务器采用了Compagy策略,那么我将为通过构造函数注入实现连接的bean配置datsource值。否则,如果我的应用是产品,并且有机会可以连接到客户的任何数据库,我将通过setter注入来实现这种数据库配置和多品牌实施。我只是举了一个例子,但是有更好的方法来实现上述场景。


1
即使是内部编码,我也总是从自己是独立承包商的角度编写代码,从而开发可重新发布的代码。我认为它将是开源的。这样,我便确保代码是模块化的和可插入的,并且遵循SOLID原则。
弗雷德

0

如果在构造函数中检查了参数,构造函数注入确实显式揭示了依赖关系,从而使代码更易读,并且不太可能发生未处理的运行时错误,但这确实取决于个人看法,并且使用DI越多,您将得到的收益就越多。倾向于根据项目来回摇摆。我个人在代码气味方面有问题,例如带有一长串参数的构造函数,并且我觉得对象的使用者应该知道依赖关系才能使用该对象,因此这为使用属性注入提供了理由。我不喜欢属性注入的隐式本质,但是我发现它更加优雅,从而使代码看起来更简洁。但另一方面,构造函数注入确实提供了更高的封装度,

根据您的特定情况,按构造器或按属性选择注入。而且不要觉得仅仅因为看起来有必要而必须使用DI,这样就可以防止不良的设计和代码气味。有时,如果付出的努力和复杂性超过了收益,那么使用这种模式就不值得付出努力。把事情简单化。

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.