如何在领域模型实体类中注入辅助依赖项

3

我正在努力将我的桌面/WPF解决方案从使用服务定位器模式转换为使用依赖注入。到目前为止,这个过程相对轻松(因为在两种情况下都使用同一个UnityContainer):我只需删除每个对全局/静态ServiceLocator的调用,并将依赖项放在构造函数中。但是当涉及到一个存在于我的实体类之一的辅助服务时,我陷入了困境。

当前我有类似以下的代码:

一个单例帮助服务,它不包含任何状态,只有一些常用逻辑:
interface ICalculationsHelper { double DoCompletelyCrazyCalculations(); }

然后,在领域模型实体中使用它:
class CrazyNumber
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double TheNumber { get; set; }

    ICalculationsHelper _helper = ServiceLocator.Resolve<ICalculationsHelper>();

    public CrazyNumber()
    {
        CreateCrazyNumber();
    }

    private void CreateCrazyNumber()
    {
        TheNumber = _helper.DoCompletelyCrazyCalculations();
    }
}

...这对于Entity Framework来说不是问题,可以实现对象的具体化,而且我可以在许多方面使用这个类(例如:包装在ViewModels中、在列表中操作等),因为我只需要处理一个默认构造函数。

如果我这样做会发生什么(移除ServiceLocator并将依赖的helper放入构造函数中):

class CrazyNumber
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double TheNumber { get; set; }

    ICalculationsHelper _helper;

    public CrazyNumber(ICalculationsHelper helper)
    {
        _helper = helper;
        CreateCrazyNumber();
    }

    private void CreateCrazyNumber()
    {
        TheNumber = _helper.DoCompletelyCrazyCalculations();
    }
}

1) EF应该如何为每个实体注入一个新的帮助器? 2) 假设我的应用程序在100个地方以各种不同的方式操纵实体,而且都使用默认构造函数。现在突然间我必须修改每个算法,手动将ICalculationsHelper传递给实体。这会导致代码混乱,并使每个算法变得复杂。根据服务定位器模式,将这个“次要”的服务静默加载到后台似乎更加清晰。 3) 如果使用服务定位器在这种情况下更好,那么该域类应该如何进行单元测试(模拟服务)?

谢谢


相关链接:https://dev59.com/4m445IYBdhLWcg3we6V9 - Steven
1个回答

3

1) EF应该如何为每个实体注入一个新的助手?

使用IoC容器并不可行。IoC容器只知道如何为它自己创建的(根)对象注入依赖项,而不是在其他情况下创建的对象。因此,如果您希望实体具有此依赖项(这是有争议的,请参见后面的内容),可以订阅包装的ObjectContextObjectMaterialized事件:

((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized += 
     ObjectContext_ObjectMaterialized;

在处理程序中,您可以检查是否实现了CrazyNumber并将ICalculationsHelper指定给它。因此,这不是构造函数注入,而是属性注入(某种程度上,而不是由IoC容器)。根据以下备注,您还可以在其中生成其Thenumber,而无需注入服务。

2) ……似乎使用服务定位器模式更加简洁。

我同意。如果由于某种原因无法使用IoC,则SL是次优选择。

3) 这个域类应该如何进行单元测试?

这是一个一般的IoC问题。对于单元测试,如果服务本身具有无法在单元测试上下文中满足的依赖项,则IoC容器应能够注入模拟服务。
但我怀疑您是否应该将此服务注入实体。这涉及到一个广泛的主题,但我会在这里给出一些想法:
  • I especially don't like the fact that the service does its work in the constructor. The object is constructed by EF, so whatever the service does, it may interfere with object construction by EF.

  • Why should CrazyNumber create its TheNumber property itself (granted that your code is just a stand-in for the real case)? From an object-oriented point of view this should happen if it combines data and behavior. In other words, if CrazyNumber contains the state that's required to generate the number. But this state can't ever be guaranteed to be complete or stable in a constructor. (It is after ObjectMaterialized). And if this state is not required, then the behavior shouldn't be there at all (single responsibility).

  • Maybe your example is a bit contrived, maybe the service is needed to create the number later, e.g. when it's first accessed. Then too, CrazyNumber shouldn't have this dependency, because there are likely to be code paths in which the number is never generated. In that case, the service is a loose dependency and it will be hard to tell when it actually needs it. It would be better to "inject" it by method injection when it's really needed:

    public void CreateCrazyNumber(ICalculationsHelper helper)
    {
        TheNumber = helper.DoCompletelyCrazyCalculations();
    }
    

感谢您的详细回答。在我编造的例子中,helper 应该代表类似于 Math.Sqrt() 的东西,但是它是特定于应用程序的,并且被几个类所需。我不想在每个需要它的类中重复相同的数值计算算法。最初,我将代码分解为静态 helper 类,然后我在某个地方读到静态类在现代面向对象应用程序中不适合此目的,因此我用服务定位单例替换了它。这就带来了现在的问题,即尝试注入服务。我错过了什么吗?谢谢。 - BCA
静态类不好?这要看情况……我认为这通常适用于静态实用程序类,就像Math一样。带有状态的静态类是不好的,但没有副作用的静态函数则可以。 - Gert Arnold
谢谢,知道逻辑专用的静态类是没问题的,感觉轻松了许多,需要跳过的步骤也少了很多。 - BCA

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接