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

时间:2012-05-14 21:26:05

标签: c# dependency-injection inversion-of-control

我在我的C#项目中使用依赖注入,通常一切正常。 尽管如此,我经常听到规则“构造函数必须只包含琐碎的操作 - 分配依赖关系而不再做任何事情”I.e:

//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值,根据依赖关系进行一些计算。)< / p>

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

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

是否有任何其他关于忽视非平凡构造函数的考虑因素?

3 个答案:

答案 0 :(得分:1)

如果您只需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);

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

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

但它违反了SRP(创建和初始化)并且难以测试。您可以阅读有关此漏洞的更多信息on writing testable code guide。主要想法:

  

不要在构造函数中创建协作者,而是将其传递给。   (不要找东西!要求东西!)

答案 1 :(得分:1)

在构造函数中不做任何工作的基本原理来自于分两个阶段查看程序的执行情况。第一阶段是连接对象图。第二阶段是做“真正的工作”。

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

摆脱这个难题的方法是认识到对象的“实际工作”是由它与其他对象相关的界面和行为来定义的。也就是说,提供给构造函数的依赖项和作为方法的参数提供的对象将在未来发展。

您可以在构造函数中自由地执行任何类型的设置,这些设置对系统中的其他对象没有明显影响。同样,对对象构造中的时序问题非常敏感。

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

在您的示例中,您必须确定foo.GetBuzz()是否会破坏该条件。如果GetBuzz()是一个引用透明的函数,那么你几乎总是在构造函数中调用它。如果GetBuzz()涉及任何I / O,用户交互或更改任何其他对象的任何明显的内部状态,则可能不需要从构造函数调用它。

答案 2 :(得分:0)

正如lazyberezovsky正确提到的那样,不要寻找东西!求东西!

如果创建代码(比方说,MyNiceCreator)将foo视为不透明值且新闻为MyNiceConsumer,那么很可能创作不应由MyNiceCreator负责}。创建MyNiceConsumer实例的代码必须能够为构造函数提供所需的值。 更好的模式: MyNiceCreator应该“询问”MyNiceConsumer个实例。这样,MyNiceConsumer实例的创建将由相应的类负责。