【问题标题】:Constructor injection best practices构造函数注入最佳实践
【发布时间】:2014-07-28 18:33:59
【问题描述】:

我有 3 个服务组件,一个负责某种数据序列化的低级服务,一个负责协调保存/加载的中间服务,一个负责 API 发布的 MVC 控制器。

这 3 个组件中的每一个在逻辑上都指向它“下方”的另一个。中间服务有另一个参数,它在运行时是已知的,基于请求数据。从这 3 个组件中,控制器和中间服务由类表示(引入接口没有意义,因为没有什么可模拟的),并且最低级别由接口表示,使其可用于对中间服务进行单元测试或控制器。我想使用 DI(特别是 Ninject)来构建我的控制器类。我的问题是是否存在处理这种情况的任何最佳实践。目前我看到了两种实现方式。 (为了清楚起见,省略了验证和正确的实现。)

首先,这里是中间服务和底层接口的示例实现。

public interface ISerializer {
  void Serialize(object data);  
}

public class MyService {
  private string _dataId;
  private ISerializer _serializer;
  public MyService(string dataId, ISerializer serializer) {
    _serializer = serializer;
    _dataId = dataId;
  }
  public bool CanProcess(MyDTO data) {
    ...
  }
  public void DoSomeProcessing(MyDTO data) {
    ...
  }
}

版本一:将整个中间服务作为工厂注入到控制器中

public class MyController : Controller {
  private Func<string, MyService> _myServiceFactory;
  public MyController(Func<string, MyService> myServiceFactory) {
    _myServiceFactory = myServiceFactory;
  }
  ...
  [HttpPost]
  public JsonResult Process(string dataId, MyDTO model) {
    using (var myService = _myServiceFactory(dataId)) {
      ...
      if (myService.CanProcess(model))
        myService.DoSomeProcessing(model);
      ...
      return Json("ok");
    }
  }  
}

版本2:直接将底层接口注入控制器,“手动”实例化中间服务。

public class MyController : Controller {
  private ISerializer _serializer;
  public MyController(ISerializer serializer) {
    _serializer = serializer;
  }
  ...
  [HttpPost]
  public JsonResult Process(string dataId, MyDTO model) {
    using (var myService = new MyService(dataId, _serializer) {
      ...
      if (myService.CanProcess(model))
        myService.DoSomeProcessing(model);
      ...
      return Json("ok");
    }
  }  
}

哪个更合适,还是我应该选择完全不同的解决方案?

【问题讨论】:

  • 为什么不让 Ninject 处理所有事情呢?只要您提供所有绑定引用,Ninject 就会在控制器加载时初始化控制器所需的内容,并一直遵循依赖链。
  • @ChrisPratt 是的,我会这样做,但我在 MyService 上有那个棘手的运行时构造函数参数。看代码的时候,我要更喜欢工厂版了……
  • 这取决于。即使依赖项是值类型,如果你能告诉它如何做,Ninject 也可以解决它。您只需要定义一个可以以某种方式返回此值的方法并告诉 Ninject 使用它。但是,如果这是您真正无法在更高级别解决的问题,那么依赖注入(至少对于您的控制器)无论如何都是不可能的。总而言之,这是一个架构问题。
  • 正如您在代码中看到的那样,该值来自请求。可能我有更高层次的设计问题,我正在考虑。

标签: c# asp.net-mvc design-patterns dependency-injection ninject


【解决方案1】:

首先,我喜欢我的服务是无状态的,所以我不喜欢传递dataId 的想法 到服务的构造函数。当服务是无状态的时,它们会更安全。您可以调用他们的方法,而不用担心它们当前是否处于有效状态。它还使测试和模拟它们变得更容易。您还可以减少使用的内存量,因为您只需要一个无状态服务实例。

如果您将dataId 移至DoSomeProcessing 作为参数,您将能够轻松地使用Ninject 实例化MyService,并且ISerializer 的正确实现将被自动注入。

但是,如果您坚持将其传递给构造函数,“版本 1”非常接近我认为好的。当构造函数中还需要数据参数时,工厂是让 DI 注入依赖项的一个很好的技巧。我会将MyServiceFactory 注入控制器。我会为它创建另一个类:

public class MyServiceFactory : IMyServiceFactory // an interface to me able to mock it if needed
{
    ISerializer _serializer;
    MyServiceFactory(ISerializer serializer){  // here Ninject can inject the dependency
        _serializer = serializer;
    }

    IMyService Create(int dataId){ // here you can pass additional parameter
        return new MyService(dataId, _serializer);
    }
}

这样您可以轻松避免硬依赖并使代码更易于维护和测试。

“版本 2”是错误的。如果你想测试你的控制器或用另一个实现替换 MyService - 你被卡住了。您将不得不进行大量繁琐的重构(取决于使用量)。最后你会得到类似于我上面建议的东西。 :)

【讨论】:

  • 谢谢,我正要得出类似的结论。我将 dataId 存储为成员变量,因为该服务充满了小方法,所有这些方法都需要该变量。但我认为我需要以某种方式对其进行重构,因为我同意你关于无状态服务的观点。再次感谢。
猜你喜欢
  • 2021-02-13
  • 2019-02-18
  • 2017-01-09
  • 1970-01-01
  • 2020-07-28
  • 2011-04-17
  • 1970-01-01
  • 1970-01-01
  • 2017-09-13
相关资源
最近更新 更多