通过构造函数或属性设置器进行依赖注入?
Posted
技术标签:
【中文标题】通过构造函数或属性设置器进行依赖注入?【英文标题】:Dependency injection through constructors or property setters? 【发布时间】:2010-12-02 23:31:26 【问题描述】:我正在重构一个类并向它添加一个新的依赖项。该类当前正在构造函数中获取其现有依赖项。所以为了一致性,我将参数添加到构造函数中。 当然,还有一些子类以及更多用于单元测试的子类,所以现在我正在玩改变所有构造函数以匹配的游戏,这需要很长时间。 这让我认为使用带有 setter 的属性是获取依赖项的更好方法。我不认为注入的依赖项应该是构造类实例的接口的一部分。您添加了一个依赖项,现在您的所有用户(子类和直接实例化您的任何人)突然都知道它了。这感觉就像是打破了封装。
这似乎不是这里现有代码的模式,所以我想找出普遍的共识是什么,构造函数与属性的优缺点。使用属性设置器更好吗?
【问题讨论】:
【参考方案1】:嗯,这取决于:-)。
如果类在没有依赖项的情况下无法完成其工作,则将其添加到构造函数中。类 需要 新的依赖项,因此您希望您的更改能够破坏事物。此外,创建一个未完全初始化的类(“两步构造”)是一种反模式(恕我直言)。
如果类可以在没有依赖项的情况下工作,那么设置器就可以了。
【讨论】:
我认为在很多情况下,最好使用空对象模式并坚持要求构造函数上的引用。这避免了所有空值检查和增加的圈复杂度。 @Mark:好点。但是,问题是关于向现有类添加依赖项。然后保持一个无参数的构造函数允许向后兼容。 什么时候需要依赖项才能运行,但默认注入该依赖项通常就足够了。那么该依赖项是否应该被属性或构造函数重载“覆盖”? @Patrick:“类无法在没有依赖项的情况下完成其工作”,我的意思是没有合理的默认值(例如,该类需要数据库连接)。在您的情况下,两者都可以。我仍然通常会选择构造函数方法,因为它降低了复杂性(如果 setter 被调用两次怎么办)? 这里有一篇很好的文章explorejava.com/different-types-of-bean-injection-in-spring【参考方案2】:类的用户应该了解给定类的依赖关系。例如,如果我有一个连接到数据库的类,并且没有提供注入持久层依赖项的方法,那么用户永远不会知道到数据库的连接必须可用。但是,如果我更改构造函数,我会让用户知道存在对持久层的依赖。
另外,为了避免你不得不改变旧构造函数的每次使用,只需应用构造函数链接作为新旧构造函数之间的临时桥梁。
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.
依赖注入的一个要点是揭示类有哪些依赖。如果类有太多的依赖关系,那么可能是时候进行一些重构了:类的每个方法是否都使用了所有的依赖关系?如果没有,那么这是一个很好的起点,可以查看可以在哪里拆分班级。
【讨论】:
当然,只有在新参数有合理的默认值时,构造函数链接才有效。但否则无论如何你都无法避免破坏...... 通常,您将使用依赖注入之前在方法中使用的任何内容作为默认参数。理想情况下,这将使新的构造函数添加成为一次干净的重构,因为类的行为不会改变。 我同意您关于资源管理依赖项(如数据库连接)的观点。我认为我的问题是我添加依赖项的类有几个子类。在 IOC 容器世界中,属性将由容器设置,使用 setter 至少可以减轻所有子类之间构造函数接口重复的压力。【参考方案3】:当然,放入构造函数意味着您可以一次验证所有内容。如果您将事物分配到只读字段中,那么您可以从构建时就对对象的依赖关系有一些保证。
添加新的依赖项确实很痛苦,但至少这样编译器会一直抱怨直到它正确为止。我认为这是一件好事。
【讨论】:
为此加一个。此外,这极大地降低了循环依赖的危险......【参考方案4】:如果您有大量可选依赖项(这已经是一种味道),那么可能 setter 注入是要走的路。不过,构造函数注入可以更好地揭示您的依赖关系。
【讨论】:
【参考方案5】:一般首选的方法是尽可能使用构造函数注入。
构造函数注入准确地说明了对象正常运行所需的依赖项 - 没有什么比新建一个对象并在调用它的方法时崩溃更烦人的了,因为没有设置一些依赖项。构造函数返回的对象应该处于工作状态。
尽量只有一个构造函数,这样可以保持设计简单并避免歧义(如果不是为了人类,也是为了 DI 容器)。
当你有 Mark Seemann 在他的“.NET 中的依赖注入”一书中所说的本地默认值时,你可以使用属性注入:依赖是可选的,因为你可以提供一个很好的工作实现但想要允许调用者在需要时指定不同的。
(下面是以前的答案)
如果注入是强制性的,我认为构造函数注入会更好。如果这添加了太多构造函数,请考虑使用工厂而不是构造函数。
如果注入是可选的,或者如果您想在中途更改它,那么 setter 注入非常好。我一般不喜欢二传手,但这是品味问题。
【讨论】:
我认为通常在中途更改注入是不好的风格(因为您正在向对象添加隐藏状态)。但当然没有例外的规则...... 是的,这就是为什么我说我不太喜欢 setter ......我喜欢构造函数的方法,因为它不能改变。 "如果这增加了太多的构造函数,考虑使用工厂而不是构造函数。"您基本上推迟了任何运行时异常,甚至可能会出错并最终陷入服务定位器实现。 @Marco 这是我以前的回答,你是对的。如果有很多构造函数,我会争辩说这个类做了太多的事情:-) 或者考虑一个抽象工厂。【参考方案6】:这在很大程度上取决于个人品味。 就我个人而言,我更喜欢 setter 注入,因为我相信它为您提供了更大的灵活性,您可以在运行时替换实现。 此外,我认为带有大量参数的构造函数并不干净,构造函数中提供的参数应仅限于非可选参数。
只要类接口 (API) 清楚地知道执行任务所需的内容, 你很好。
【讨论】:
请说明您投反对票的原因? 是的,有很多参数的构造函数是不好的。这就是为什么你用大量构造函数参数重构类:-)。 @nkr1pt:大多数人(包括我在内)都同意 setter 注入是不好的,如果它允许您创建一个在未完成注入的情况下在运行时失败的类。因此,我相信有人反对您关于个人品味的说法。【参考方案7】:我个人更喜欢 Extract and Override “模式”而不是在构造函数中注入依赖项,主要是出于您问题中概述的原因。您可以将属性设置为virtual
,然后在派生的可测试类中覆盖实现。
【讨论】:
我认为该模式的正式名称是“模板方法”。【参考方案8】:我更喜欢构造函数注入,因为它有助于“强制”类的依赖要求。如果它在 c'tor 中,消费者必须设置对象以使应用程序编译。如果您使用 setter 注入,他们可能直到运行时才知道他们有问题 - 并且取决于对象,它可能会在运行时延迟。
当注入的对象本身可能需要大量工作(例如初始化)时,我仍然不时使用 setter 注入。
【讨论】:
【参考方案9】:我更喜欢构造函数注入,因为这看起来最合乎逻辑。就像说我的班级需要这些依赖项来完成它的工作。如果它是一个可选依赖项,那么属性似乎是合理的。
我还使用属性注入来设置容器没有引用的内容,例如使用容器创建的演示者上的 ASP.NET 视图。
我认为它不会破坏封装。内部工作应该保持在内部,并且依赖关系处理不同的问题。
【讨论】:
感谢您的回答。显然构造函数是流行的答案。但是我确实认为它以某种方式破坏了封装。预依赖注入,该类将声明和实例化它完成其工作所需的任何具体类型。使用 DI,子类(和任何手动实例化器)现在知道基类使用什么工具。您添加了一个新的依赖项,现在您必须从所有子类中链接实例,即使它们不需要自己使用该依赖项。 刚刚写了一个不错的长答案,但由于本网站的异常而丢失了!! :( 总之,基类通常用于重用逻辑。这个逻辑很容易进入子类......所以你可以想到基类和 subclass= 一个关注点,它依赖于多个外部对象,即做不同的工作。你有依赖关系的事实并不意味着你需要公开任何你以前保密的东西。【参考方案10】:可能值得考虑的一个选项是从简单的单个依赖项中组合复杂的多重依赖项。也就是说,为复合依赖定义额外的类。这使得 WRT 构造函数注入变得更容易 - 每次调用的参数更少 - 同时仍然保持必须提供所有依赖项以实例化的事情。
当然,如果存在某种逻辑依赖关系分组,则最有意义,因此复合不仅仅是任意聚合,如果单个复合依赖项有多个依赖项,则最有意义 - 但参数块“模式”已经存在了很长时间,而我看到的大多数都是相当随意的。
不过,就个人而言,我更喜欢使用方法/属性设置器来指定依赖项、选项等。调用名称有助于描述正在发生的事情。但是,最好提供示例 this-is-how-to-set-it-up sn-ps,并确保依赖类进行了足够的错误检查。您可能希望使用有限状态模型进行设置。
【讨论】:
【参考方案11】:我最近ran into a situation 在一个类中有多个依赖项,但每个实现中只有一个依赖项必然会发生变化。由于数据访问和错误记录依赖项可能仅出于测试目的而更改,因此我为这些依赖项添加了可选参数,并在我的构造函数代码中提供了这些依赖项的默认实现。这样,该类将保持其默认行为,除非被该类的使用者覆盖。
使用可选参数只能在支持它们的框架中完成,例如 .NET 4(对于 C# 和 VB.NET,尽管 VB.NET 一直都有它们)。当然,您可以通过简单地使用可以由您的类的使用者重新分配的属性来完成类似的功能,但是您不会获得通过将私有接口对象分配给构造函数的参数所提供的不变性的优势。
话虽如此,如果您要引入每个消费者都必须提供的新依赖项,那么您将不得不重构您的构造函数以及消费者您的类的所有代码。我上面的建议真的只适用于你能够为所有当前代码提供默认实现,但仍然能够在必要时覆盖默认实现的能力。
【讨论】:
【参考方案12】:构造函数注入确实显式地揭示了依赖关系,如果在构造函数中检查参数,则使代码更具可读性并且更不容易出现未处理的运行时错误,但它确实归结为个人意见,并且您使用 DI 的次数越多根据项目的不同,您往往会以一种或另一种方式来回摇摆。我个人对代码闻起来有问题,比如带有一长串参数的构造函数,而且我觉得对象的使用者应该知道依赖关系才能使用该对象,因此这为使用属性注入提供了一个案例。我不喜欢属性注入的隐含性质,但我发现它更优雅,代码看起来更干净。但另一方面,构造函数注入确实提供了更高程度的封装,根据我的经验,我会尽量避免使用默认构造函数,因为如果不小心,它们会对封装数据的完整性产生不良影响。
根据您的具体情况明智地选择按构造函数或按属性注入。并且不要仅仅因为它看起来有必要就认为你必须使用 DI,它会防止糟糕的设计和代码异味。有时,如果付出的努力和复杂性超过了好处,那么使用模式是不值得的。保持简单。
【讨论】:
【参考方案13】:这是一个旧帖子,但如果将来需要它,也许这有任何用处:
https://github.com/omegamit6zeichen/prinject
我有类似的想法并想出了这个框架。它可能还远未完成,但它是一个专注于属性注入的框架的想法
【讨论】:
【参考方案14】:这取决于您要如何实施。 我更喜欢构造函数注入,只要我觉得进入实现的值不会经常改变。例如:如果公司战略与 oracle 服务器一起使用,我将为通过构造函数注入实现连接的 bean 配置我的 datsource 值。 否则,如果我的应用程序是一个产品并且它可以连接到客户的任何数据库,我会通过 setter 注入来实现这样的数据库配置和多品牌实现。我刚刚举了一个例子,但是有更好的方法来实现我上面提到的场景。
【讨论】:
即使在内部编码时,我总是从我是一个独立承包商的角度进行编码,开发旨在可再分发的代码。我认为它将是开源的。通过这种方式,我确保代码是模块化和可插拔的,并且遵循 SOLID 原则。【参考方案15】:何时使用构造函数注入? 当我们要确保创建对象时包含其所有依赖项并确保所需的依赖项不为空。
何时使用 Setter 注入? 当我们使用可以在类中分配合理默认值的可选依赖项时。否则,必须在代码使用依赖项的任何地方执行非空检查。 此外,setter 方法使该类的对象可以在以后重新配置或重新注入。
来源: Spring documentation , Java Revisited
【讨论】:
以上是关于通过构造函数或属性设置器进行依赖注入?的主要内容,如果未能解决你的问题,请参考以下文章