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

本文关键字:两步 初始化 模式 依赖 注入 副作用 | 更新日期: 2023-09-27 18:23:47

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

示例:

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 Container配置类中将其配置为命名参数,但是,我们不喜欢在配置文件中创建"new namedParameter(paramvalue)’s"的想法,因为这会使其不必要地不可读,并造成未来的维护痛点。

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

问题:我读到一些人认为这两步初始化是反模式的。如果这是共识,这难道不意味着通过IoC容器注入依赖关系的方法存在某种局限性/弱点吗?

编辑:在研究了Mark Seeman的建议之后:

关于这个问题的答案,我有几点意见:初始化/应用:同意它是一种反模式/气味。Yacoub-Massad:我同意IoC容器在原始依赖关系方面是个问题。这里描述的手动(穷人的)DI对于较小或体系结构稳定的系统来说听起来很棒,但我认为维护大量手动配置的组合根可能会变得非常困难。

选项:1) 工厂作为依赖项(当需要运行时解析时)2) 如本文所述,将有状态对象与纯服务分离。

(1) :这就是我一直在做的事情,但我意识到有可能引发另一种反模式:服务定位器。(2) :我更喜欢我的特殊情况,因为我可以干净地将这两种类型分开。纯服务是一个无需思考的IoC容器,而有状态的对象解析将取决于它们是否具有原始依赖关系。

每次我"不得不"使用依赖注入时,它都会以一种教条主义的方式使用,通常是在主管的命令下,他决心不惜任何代价将DI应用到IoC容器中。

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

我读到一些认为这两步初始化反模式

Initialize方法导致了时间耦合。称它为反模式可能过于严格,但它肯定是一种设计气味。

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

  • 配置值:如果它是在组件生命周期内不会更改的常量/配置值,则应将该值直接注入构造函数。

  • 运行时值:如果值在运行时发生更改(例如请求特定的值),则在初始化期间不应提供该值(既不通过构造函数也不使用某些Initialize方法)。用运行时数据初始化组件实际上是一种反模式。

我部分同意@YacoubMassad关于使用DI容器配置基元依赖关系的观点。当使用自动连接时,容器提供的API无法以可维护的方式设置这些值。我认为这主要是由于C#和.NET的限制。在设计和开发Simple Injector时,我很长一段时间都在使用这样的API,但我决定完全放弃这样的API,因为我没有找到一种既直观又能为用户提供易于维护的代码的方法来定义API。因此,我通常建议开发人员将原始类型提取到参数对象中,而不是将参数对象注册并注入到消费类型中。换句话说,TaxRate属性可以封装在ProductServiceSettings类中,并且该Parameter Object可以注入到ProductProcessor中。

但正如我所说,我只是部分同意雅库布的观点。尽管手工编写一些对象(也称为纯DI)更实用,但他暗示这意味着你应该完全放弃DI容器。国际海事组织的说法过于强硬。在我编写的大多数应用程序中,我使用容器批量注册大约98%的类型,并手动连接另外两个2%,因为自动连接它们太复杂了。这为在我的应用程序上下文中提供了最佳的总体结果。当然,你的里程数可能会有所不同。并不是每个应用程序都能从使用DI容器中获益,而且我自己在编写的所有应用程序中也没有使用容器。但是,我总是应用依赖注入模式和SOLID原则。

示例中的taxRate是Primitive Dependency。基元依赖关系应该像其他依赖关系一样正常地注入构造函数中。以下是构造函数的样子:

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

在我看来,DI容器不能很好地/容易地支持原始依赖性是DI容器的一个问题/弱点。

在我看来,最好使用Pure DI进行对象组合,而不是使用DI容器。一个原因是它支持更容易地注入基元依赖关系。另一个原因请参阅本文。

使用Initialize方法存在一些问题。它要求调用Initialize方法,从而使对象的构造更加复杂。此外,程序员可能会忘记调用Initialize方法,这会使对象处于无效状态。这也意味着本例中的taxRate是一个隐藏的依赖项。程序员仅仅通过查看构造函数就不会知道你的类依赖于这样的基元依赖。

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