我正在重构一个类并向它添加一个新的依赖项。该类当前正在构造函数中获取其现有依赖项。所以为了一致性,我将参数添加到构造函数中。当然,还有一些子类以及更多用于单元测试的子类,所以现在我正在玩改变所有构造函数以匹配的游戏,这需要很长时间。这让我认为使用带有 setter 的属性是获取依赖项的更好方法。我不认为注入的依赖项应该是构造类实例的接口的一部分。您添加了一个依赖项,现在您的所有用户(子类和直接实例化您的任何人)突然都知道它了。这感觉就像是封装的破裂。
这似乎不是这里现有代码的模式,所以我想找出普遍的共识是什么,构造函数与属性的优缺点。使用属性设置器更好吗?
这得看情况 :-)。
如果类在没有依赖项的情况下无法完成工作,则将其添加到构造函数中。该类需要新的依赖项,因此您希望您的更改能够破坏事物。此外,创建一个未完全初始化的类(“两步构造”)是一种反模式(恕我直言)。
如果该类可以在没有依赖关系的情况下工作,那么 setter 就可以了。
类的用户应该知道给定类的依赖关系。例如,如果我有一个连接到数据库的类,并且没有提供注入持久层依赖项的方法,那么用户永远不会知道到数据库的连接必须可用。但是,如果我更改构造函数,我会让用户知道存在对持久层的依赖。
此外,为了避免您不得不更改旧构造函数的每次使用,只需应用构造函数链接作为旧构造函数和新构造函数之间的临时桥梁。
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.
}
}
依赖注入的要点之一是揭示类具有哪些依赖项。如果类有太多的依赖关系,那么可能是时候进行一些重构了:类的每个方法是否都使用了所有的依赖关系?如果不是,那么这是一个很好的起点,可以查看可以在哪里拆分班级。
当然,装上构造函数意味着您可以一次验证所有内容。如果您将事物分配到只读字段中,那么您可以从构建时就对对象的依赖关系有一些保证。
添加新的依赖项确实很痛苦,但至少这样编译器会一直抱怨直到它正确为止。我认为这是一件好事。
如果您有大量可选依赖项(这已经是一种气味),那么可能 setter 注入是要走的路。不过,构造函数注入可以更好地揭示您的依赖关系。
一般首选的方法是尽可能使用构造函数注入。
构造函数注入准确地说明了对象正常运行所需的依赖项 - 没有什么比新建一个对象并在调用它的方法时崩溃更烦人的了,因为没有设置一些依赖项。构造函数返回的对象应该处于工作状态。
尽量只有一个构造函数,它使设计保持简单并避免歧义(如果不是为了人类,对于 DI 容器)。
当你有 Mark Seemann 在他的“.NET 中的依赖注入”一书中所说的本地默认值时,你可以使用属性注入:依赖是可选的,因为你可以提供一个很好的工作实现,但希望允许调用者指定一个不同的如果需要。
(下面是以前的答案)
如果注入是强制性的,我认为构造函数注入会更好。如果这添加了太多构造函数,请考虑使用工厂而不是构造函数。
如果注入是可选的,或者如果您想在中途更改它,则 setter 注入很好。我一般不喜欢二传手,但这是一个品味问题。
这在很大程度上是个人品味的问题。就个人而言,我更喜欢 setter 注入,因为我相信它为您提供了更大的灵活性,您可以在运行时替换实现。此外,在我看来,具有大量参数的构造函数并不干净,并且构造函数中提供的参数应仅限于非可选参数。
只要类接口(API)清楚地知道它需要什么来执行它的任务,你就很好。
我更喜欢构造函数注入,因为它有助于“强制”类的依赖要求。如果它在 c'tor 中,则使用者必须设置对象以使应用程序编译。如果您使用 setter 注入,他们可能直到运行时才知道他们有问题 - 并且取决于对象,它可能会在运行时延迟。
当注入的对象本身可能需要大量工作(例如初始化)时,我仍然不时使用 setter 注入。
我个人更喜欢 Extract and Override “模式”而不是在构造函数中注入依赖项,主要是出于您问题中概述的原因。您可以将属性设置为 virtual
,然后在派生的可测试类中覆盖实现。
我更喜欢构造函数注入,因为这似乎最合乎逻辑。就像说我的班级需要这些依赖项才能完成工作。如果它是一个可选的依赖项,那么属性似乎是合理的。
我还使用属性注入来设置容器没有引用的内容,例如使用容器创建的演示者上的 ASP.NET 视图。
我认为它不会破坏封装。内部工作应该保持在内部,并且依赖关系处理不同的问题。
可能值得考虑的一个选项是从简单的单个依赖项中组合复杂的多重依赖项。也就是说,为复合依赖定义额外的类。这使得 WRT 构造函数注入变得更容易 - 每次调用的参数更少 - 同时仍然保持必须提供所有依赖项以实例化的事情。
当然,如果存在某种逻辑依赖关系分组,则最有意义,因此复合不仅仅是任意聚合,如果单个复合依赖项有多个依赖项,则最有意义 - 但参数块“模式”具有已经存在了很长时间,而我所见过的大多数都是相当随意的。
不过,就个人而言,我更喜欢使用方法/属性设置器来指定依赖项、选项等。调用名称有助于描述正在发生的事情。不过,最好提供示例 this-is-how-to-set-it-up 片段,并确保依赖类进行了足够的错误检查。您可能希望使用有限状态模型进行设置。
我最近ran into a situation在一个类中有多个依赖项,但每个实现中只有一个依赖项必然会发生变化。由于数据访问和错误记录依赖项可能仅出于测试目的而更改,因此我为这些依赖项添加了可选参数,并在我的构造函数代码中提供了这些依赖项的默认实现。这样,类将保持其默认行为,除非被类的使用者覆盖。
使用可选参数只能在支持它们的框架中完成,例如 .NET 4(对于 C# 和 VB.NET,尽管 VB.NET 一直都有它们)。当然,您可以通过简单地使用可以由您的类的使用者重新分配的属性来完成类似的功能,但是您不会获得通过将私有接口对象分配给构造函数的参数所提供的不变性的优势。
综上所述,如果您要引入每个消费者都必须提供的新依赖项,那么您将不得不重构您的构造函数以及消费者类的所有代码。我上面的建议仅适用于您能够为所有当前代码提供默认实现但仍提供在必要时覆盖默认实现的能力的情况。
构造函数注入确实显式揭示了依赖关系,如果在构造函数中检查参数,则使代码更具可读性并且更不容易出现未处理的运行时错误,但这确实归结为个人意见,并且您使用 DI 的次数越多,您将越多根据项目的不同,往往会以一种或另一种方式来回摇摆。我个人对代码闻起来像带有一长串参数的构造函数有问题,而且我认为对象的使用者应该知道依赖关系才能使用该对象,因此这为使用属性注入提供了一个案例。我不喜欢属性注入的隐含性质,但我发现它更优雅,代码看起来更干净。但另一方面,构造函数注入确实提供了更高程度的封装,根据我的经验,我尽量避免使用默认构造函数,因为如果不小心,它们会对封装数据的完整性产生不良影响。
根据您的具体情况,明智地选择按构造函数或按属性注入。并且不要仅仅因为它看起来有必要就认为你必须使用 DI,它会防止糟糕的设计和代码异味。有时,如果付出的努力和复杂性超过了好处,那么使用模式就不值得付出努力。把事情简单化。
这是一个旧帖子,但如果将来需要它,也许这有任何用处:
https://github.com/omegamit6zeichen/prinject
我有一个类似的想法,并想出了这个框架。它可能还远未完成,但它是一个专注于属性注入的框架的想法
这取决于您要如何实施。我更喜欢构造函数注入,只要我觉得进入实现的值不会经常改变。例如:如果公司战略与 oracle 服务器一起使用,我将为通过构造函数注入实现连接的 bean 配置我的 datsource 值。否则,如果我的应用程序是一个产品并且它可以连接到客户的任何数据库,我会通过 setter 注入来实现这样的数据库配置和多品牌实现。我刚刚举了一个例子,但是有更好的方法来实现我上面提到的场景。
什么时候使用构造函数注入?当我们要确保创建对象时包含其所有依赖项并确保所需的依赖项不为空。
何时使用 Setter 注入?当我们使用可以在类中分配合理默认值的可选依赖项时。否则,必须在代码使用依赖项的任何地方执行非空检查。此外,setter 方法使该类的对象可以在以后重新配置或重新注入。
来源:Spring documentation、Java Revisited
不定期副业成功案例分享