我不喜欢基于构造函数的依赖注入。
我认为这会增加代码复杂性并降低可维护性,我想知道是否有任何可行的替代方案。
我不是在谈论将实现与接口分离的概念,而是有一种从接口动态解析(递归)一组对象的方法。我完全支持这一点。但是,基于传统构造函数的方法似乎有一些问题。
1)所有测试都取决于构造函数。
去年在一个MVC 3 C#项目中广泛使用DI后,我发现我们的代码中包含以下内容:
public interface IMyClass {
...
}
public class MyClass : IMyClass {
public MyClass(ILogService log, IDataService data, IBlahService blah) {
_log = log;
_blah = blah;
_data = data;
}
...
}
问题:如果我在实现中需要其他服务,我必须修改构造函数;这意味着该类的所有单元测试都会中断。
即使与新功能无关的测试也需要至少重构以添加其他参数并为该参数注入模拟。
这似乎是一个小问题,像resharper这样的自动化工具有所帮助,但是当这样的简单更改导致100多个测试中断时,它肯定很烦人。实际上我已经看到人们在做蠢事以避免改变构造函数而不是咬紧牙关并在发生这种情况时修复所有测试。
2)服务实例不必要地传递,增加了代码复杂性。
public class MyWorker {
public MyWorker(int x, IMyClass service, ILogService logs) {
...
}
}
如果我可以在给定服务可用并且已经自动解析的上下文(例如控制器)内,或者不幸的是,通过将服务实例传递给多个辅助类链来创建此类的实例。
我一直看到像这样的代码:
public class BlahHelper {
// Keep so we can create objects later
var _service = null;
public BlahHelper(IMyClass service) {
_service = service;
}
public void DoSomething() {
var worker = new SpecialPurposeWorker("flag", 100, service);
var status = worker.DoSomethingElse();
...
}
}
如果示例不清楚,我所说的是将已解决的DI接口实例向下传递到无理由的多个层,而不是在底层需要注入某些东西
如果某个类不依赖于某个服务,那么它应该依赖于该服务。在我看来,这种观点认为存在一种“瞬态”依赖关系,即一个类不会使用一个服务而只是简单地传递它,这是无意义的。
但是,我不知道更好的解决方案。
有没有提供DI的好处而没有这些问题?
我已经考虑在构造函数中使用DI框架,因为这解决了一些问题:
public MyClass() {
_log = Register.Get().Resolve<ILogService>();
_blah = Register.Get().Resolve<IBlahService>();
_data = Register.Get().Resolve<IDataService>();
}
这样做有什么缺点吗?
这意味着单元测试必须具有类的“先验知识”,以便在测试初始化期间将mocks绑定到正确的类型,但我看不到任何其他缺点。
NB。我的例子是在c#中,但我也遇到了其他语言中的相同问题,尤其是那些工具支持不太成熟的语言,这些都是令人头疼的问题。
答案 0 :(得分:18)
在我看来,所有问题的根本原因都在于没有正确的DI。使用构造函数DI的主要目的是清楚地说明某些类的所有依赖关系。如果某些东西取决于某些东西,你总是有两个选择:明确这种依赖或将其隐藏在某种机制中(这种方式往往带来比利润更多的麻烦)。
通过你的陈述:
所有测试都取决于构造函数。
[剪断]
问题:如果我在实现中需要其他服务,我必须修改构造函数;这意味着该类的所有单元测试都会中断。
让一个班级取决于其他一些服务是一个相当重大的变化。如果您有多个服务实现相同的功能,我会认为存在设计问题。正确的模拟和测试满足SRP(在单元测试方面归结为“为每个测试用例编写单独的测试”)并且独立应该解决这个问题。
2)服务实例不必要地传递,增加了代码复杂性。
DI的最常见用途之一是将对象创建与业务逻辑分开。在你的情况下,我们看到你真正需要的是创建一些Worker,而这又需要通过整个对象图注入的几个依赖项。解决此问题的最佳方法是永远不要在业务逻辑中执行任何new
。对于这种情况,我宁愿注入一个工人工厂,从工人的实际创建中抽象出业务代码。
我已经考虑在构造函数中使用DI框架,因为这解决了一些问题:
public MyClass() { _log = Register.Get().Resolve<ILogService>(); _blah = Register.Get().Resolve<IBlahService>(); _data = Register.Get().Resolve<IDataService>(); }
这样做有什么缺点吗?
作为一种好处,您将获得使用Singleton
模式的所有缺点(不可测试的代码和应用程序的巨大状态空间)。
所以我会说DI应该做得对(就像任何其他工具一样)。解决问题的方法(IMO)在于理解DI和团队成员的教育。
答案 1 :(得分:15)
对于这些问题,构造函数注入很容易出错,但它们实际上是不正确实现的症状,而不是构造函数注入的缺点。
让我们分别看看每个明显的问题。
所有测试都取决于构造函数。
这里的问题实际上是单元测试与构造函数紧密耦合。这通常可以通过简单的SUT Factory来解决 - 这个概念可以扩展为Auto-mocking Container。
在任何情况下使用构造函数注入时,constructors should be simple因此没有理由直接测试它们。它们是实现细节,是您编写的行为测试的副作用。
服务实例不必要地传递,增加了代码复杂性。
同意,这肯定是代码味道,但同样,气味在实施中。构造函数注入不能归咎于此。
当发生这种情况时,这是一个Facade Service缺失的症状。而不是这样做:
public class BlahHelper {
// Keep so we can create objects later
var _service = null;
public BlahHelper(IMyClass service) {
_service = service;
}
public void DoSomething() {
var worker = new SpecialPurposeWorker("flag", 100, service);
var status = worker.DoSomethingElse();
// ...
}
}
这样做:
public class BlahHelper {
private readonly ISpecialPurposeWorkerFactory factory;
public BlahHelper(ISpecialPurposeWorkerFactory factory) {
this.factory = factory;
}
public void DoSomething() {
var worker = this.factory.Create("flag", 100);
var status = worker.DoSomethingElse();
// ...
}
}
关于提议的解决方案
建议的解决方案是服务定位器和it has only disadvantages and no benefits。
答案 2 :(得分:0)
因此,在您创建一个Context对象之前,我已经听说过一种编码模式,该对象在整个代码中都随处可见。 Context对象包含您要控制环境的任何“注入”和/或服务。
它具有GV的一般感觉,但是您对它们有更多的控制权,因为对于包外部的任何内容,您都可以将它们默认设置为null,因此您可以在包中进行测试,以访问受保护的构造方法,从而允许您控制Context对象。
例如主类:
public class Main {
public static void main(String[] args) {
// Making main Object-oriented
Main mainRunner = new Main(null);
mainRunner.mainRunner(args);
}
private final Context context;
// This is an OO alternative approach to Java's main class.
protected Main(String config) {
context = new Context();
// Set all context here.
if (config != null || "".equals(config)) {
Gson gson = new Gson();
SomeServiceInterface service = gson.fromJson(config, SomeService.class);
context.someService = service;
}
}
public void mainRunner(String[] args) {
ServiceManager manager = new ServiceManager(context);
/**
* This service be a mock/fake service, could be a real service. Depends on how
* the context was setup.
*/
SomeServiceInterface service = manager.getSomeService();
}
}
示例测试类:
public class MainTest {
@Test
public void testMainRunner() {
System.out.println("mainRunner");
String[] args = null;
Main instance = new Main("{... json object for mocking ...}");
instance.mainRunner(args);
}
}
注意,这是一些额外的工作,因此我可能只会将其用于微服务和小型应用程序。大型应用程序只需进行依赖关系注入就更容易。