C# 依赖注入副作用(两步初始化反模式)?

C# Dependency injection side effect (two step initialization anti-pattern)?

我正在开发一个项目,其中我的构造函数仅包含行为依赖项。即我从不传递值/状态。

示例:

class ProductProcessor : IProductProcessor
{
   public double SomeMethod(){ ... }
}
class PackageProcessor
{
   private readonly IProductProcessor _productProcessor;
   private double _taxRate;

   public PackageProcessor(IProductProcessor productProcessor)
   {
        _productProcessor = productProcessor;
   }

   public Initialize(double taxRate)
   {
       _taxRate = taxRate;
       return this;
   }

   public double ProcessPackage()
   {
       return _taxRate * _productProcessor.SomeMethod();
   }

}

为了传递状态,决定包括第二步(调用 Initialize)。

我知道我们可以在 IoC 容器配置 class 中将其配置为命名参数,但是,我们不喜欢在配置文件中创建 "new namedParameter(paramvalue)'s" 的想法,因为这会造成不必要的麻烦不可读并造成未来的维护痛点。

我在不止一个地方看到过这种模式。

问题:我读到一些文章认为这两个步骤初始化是一种反模式。如果这是共识,这是否意味着通过 IoC 容器进行依赖注入的方法存在某种限制/弱点?

编辑: 在查看 Mark Seeman 的 suggestion 之后:

对于这个问题的答案,我有几点评论: Initialize/Apply :同意它是一种反模式/气味。 Yacoub Massad:我同意 IoC 容器在涉及原始依赖项时是一个问题。手动(穷人的)DI,如所述 here 听起来很适合小型或架构稳定的系统,但我认为维护许多手动配置的合成根可能变得非常困难。

选项: 1)工厂作为依赖(当需要运行时间分辨率时) 2) 如here.

所述,将有状态对象与纯服务分开

(1):这就是我一直在做的事情,但我意识到有可能引发另一种反模式:服务定位器。 (2):我对我的特殊情况的偏好是关于这个的,因为我可以清楚地区分这两种类型。纯服务是一个没有脑子的 - IoC 容器,而有状态的对象解析将取决于它们是否具有原始依赖关系。

每次我'had'使用依赖注入时,它都是以教条的方式使用的,通常是在主管的命令下不惜一切代价将 DI 与 IoC 容器一起使用。

您示例中的 taxRatePrimitive Dependency。并且原始依赖项应该像其他依赖项一样在构造函数中正常注入。这是构造函数的样子:

public PackageProcessor(IProductProcessor productProcessor, double taxRate)
{
    _productProcessor = productProcessor;
    _taxRate = taxRate;
}

DI 容器不 nicely/easily 支持原始依赖这一事实在我看来是 problem/weakness DI 容器。

在我看来,出于另一个原因,最好使用 Pure DI for object composition instead of a DI container. One reason is that it supports easier injection of primitive dependencies. See this article

使用Initialize方法有一些问题。它通过要求调用 Initialize 方法使对象的构造更加复杂。此外,程序员可能会忘记调用 Initialize 方法,这会使您的对象处于无效状态。这也意味着本例中的 taxRate 是一个隐藏的依赖项。程序员不会通过简单地查看构造函数就知道您的 class 依赖于这种原始依赖性。

Initialize 方法的另一个问题是它可能会被调用两次,但值不同。另一方面,构造函数确保依赖关系不会改变。您需要创建一个特殊的布尔变量(例如 isInitialized)来检测是否已经调用了 Initialize 方法。这只会让事情变得复杂。

I read some consider this two step initialization an anti-pattern

Initialize 方法导致 Temporal Coupling. Calling it an anti-pattern might be too strict, but it sure is a Design Smell

如何向组件提供这个值取决于它是什么类型的值。有两种风格:配置值和运行时值:

  • Configuration Values:如果是constant/configuration值,在组件的生命周期内不会改变,则应该注入该值直接进入构造函数。

  • 运行时值:如果值在运行时发生变化(例如请求特定值),该值应该而不是 在初始化期间提供(既不通过构造函数也不使用某些 Initialize 方法)。使用运行时数据初始化组件实际上 IS an anti-pattern.

我部分同意@YacoubMassad 关于使用 DI 容器配置原始依赖项的观点。使用自动装配时,容器提供的 APIs 无法以可维护的方式设置这些值。我认为这主要是由 C# 和 .NET 的限制引起的。在设计和开发 Simple Injector 时,我为这样的 API 苦苦挣扎了很长时间,但决定完全放弃这样的 API,因为我没有找到定义 API 的方法] 这既直观又导致代码易于用户维护。因此,我通常建议开发人员将原始类型提取到参数对象中,而不是将参数对象注册并注入到使用类型中。换句话说,一个 TaxRate 属性 可以包裹在一个 ProductServiceSettings class 中,这个参数对象可以注入到 ProductProcessor.

但正如我所说,我只是部分同意 Yacoub 的观点。尽管手动组合一些对象更为实际 (a.k.a. Pure DI),但他暗示这意味着您应该完全放弃 DI 容器。 IMO 说的太过强烈了。在我编写的大多数应用程序中,我使用容器批量注册了大约 98% 的类型,我手动连接了另外两个 2%,因为自动连接它们太复杂了。这使 在我的应用程序上下文中 获得了最好的整体结果。当然,你的里程可能会有所不同。并不是每个应用程序都真正受益于使用 DI 容器,而且我自己并没有在我编写的所有应用程序中都使用容器。但是我一直在做的是应用依赖注入模式和 SOLID 原则。