IoC 范式中的非平凡构造函数是一件坏事吗?

本文关键字:一件 范式 构造函数 IoC | 更新日期: 2023-09-27 18:34:12

我在我的 C# 项目中使用依赖注入,通常一切都很好。尽管如此,我经常听到规则"构造函数必须只包含琐碎的操作 - 分配依赖项,什么都不做",即:

//dependencies
interface IMyFooDependency
{
   string GetBuzz();
   int DoOtherStuff();
}
interface IMyBarDependency
{
  void CrunchMe();
}
//consumer
class MyNiceConsumer
{
  private readonly IMyFooDependency foo;
  private readonly  IMyBarDependency bar;
  private /*readonly*/ string buzz;//<---question here
  MyNiceConsumer(IMyFooDependency foo, IMyBarDependency bar)
  { 
     //omitting null checks
     this.foo = foo;
     this.bar = bar;
     //OR
     this.buzz = foo.GetBuzz();//is this a bad thing to do? 
  }  
}

UPD:假设IMyFooDependency不能用GetBuzz()代替,因为在这种情况下,答案很明显:"不要依赖foo"。

UPD2:请理解,这个问题不是关于在假设代码中消除foo的依赖,而是关于理解良好构造函数设计的原则。

所以,我的问题是:在构造函数中包含非平凡逻辑(即获取buzz值,根据依赖项进行一些计算(真的是一种糟糕的模式吗?

就我个人而言,除非需要延迟加载,否则我会在构造函数中包含foo.GetBuzz(),因为对象需要在调用其构造函数后初始化。

我看到的唯一缺点:通过包含非平凡的逻辑,您可以增加可能出错的地方的数量,并且您会从 IoC 容器收到一条混淆的错误消息(但在参数无效的情况下会发生同样的事情,所以缺点相当小(

省略非平凡构造函数的任何其他注意事项?

IoC 范式中的非平凡构造函数是一件坏事吗?

如果你只需要IMyFooDependency buzz创作,那么你实际上需要嗡嗡声:

class MyNiceConsumer
{
  private readonly IMyBarDependency bar;
  private readonly string buzz;
  MyNiceConsumer(string buzz, IMyBarDependency bar)
  { 
     this.buzz = buzz;
     this.bar = bar;
  }  
}

并以这种方式创建良好消费者的实例:

new MyNiceConsumer(foo.GetBuzz(), bar);

我认为在将参数传递给构造函数之前获取嗡嗡声与在构造函数中获取它之间没有任何区别。相同的值将从存储库返回。因此,您不需要依赖存储库。

更新:从技术上讲,构造函数中的复杂初始化逻辑没有任何问题。看看 winforms InitializeComponent方法,其中所有控件都已创建、初始化并添加到窗体中。

但它违反了SRP(创建和初始化(,并且很难测试。您可以在编写可测试代码指南时阅读有关此缺陷的更多信息。主要思想:

不要在构造函数中创建协作器,而是将它们传入。 (不要找东西!问东西!

不在构造函数中执行任何工作的理由来自分两个阶段查看程序的执行。第一阶段是连接你的对象图。第二阶段是做"真正的工作"。

这种理想与有效维护类的不变性和内部状态之间存在紧张关系。在构造函数中可以执行的设置越少,所有方法的实现就越困难,因为它们必须考虑对象可能的不同内部状态。请记住,构造函数是可以确定为对象调用的唯一代码。

摆脱这个难题的方法是认识到一个对象的"实际工作"是由它与其他对象的界面和行为定义的。也就是说,提供给构造函数的依赖项和作为参数提供给方法的对象稍后会提供。

随意在构造函数中进行任何您喜欢的设置,这些设置不会对系统中的其他对象产生明显影响。同样,对对象构造中的时序问题要非常敏感。

如果确定 File 对象在没有用户提供的文件名的情况下无法存在:不要在构造函数中调用 keyboard.filename_from_keyboard((。相反,您将系统设计为在执行期间由工厂(提供程序(使用提供给构造函数的文件名创建对象,或者允许 File 对象在没有文件名的情况下存在。也许它可以在执行过程中获取自己的文件名?这是管理对象生命周期的一部分,也是IMO最难的部分。这变得非常微妙,因为"实际工作"也涉及创建对象。但是我离题了...

在您的示例中,您必须决定是否 foo。GetBuzz(( 打破了这种情况。如果 GetBuzz(( 是一个引用透明的函数,你几乎总是可以在构造函数中调用它。如果 GetBuzz(( 涉及任何 I/O、用户交互或更改任何其他对象的任何明显的内部状态,那么它可能不需要从构造函数调用。

正如懒惰别列佐夫斯基正确提到的,不要寻找东西!要东西!

如果创建代码(比如说MyNiceCreator(将foo视为不透明的值和新闻MyNiceConsumer,那么很可能创建不应该是MyNiceCreator的责任。创建 MyNiceConsumer 实例的代码必须能够为构造函数提供所需的值。更好的模式: MyNiceCreator应该"请求"一个MyNiceConsumer实例。这样MyNiceConsumer实例的创建将由相应的类负责。