A viable alternative to dependency injection?
我不喜欢基于构造函数的依赖注入。
我相信它增加了代码复杂性,降低了可维护性,我想知道是否有可行的替代方案。
我不是在讨论将实现与接口分离的概念,而是有一种从接口动态解析(递归)一组对象的方法。我完全支持。然而,传统的基于构造函数的方法似乎有一些问题。
1)所有测试都取决于施工人员。
在去年的MVC 3 C项目中广泛使用DI之后,我发现我们的代码中充满了这样的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public interface IMyClass { ... } public class MyClass : IMyClass { public MyClass(ILogService log, IDataService data, IBlahService blah) { _log = log; _blah = blah; _data = data; } ... } |
问题:如果我的实现中需要另一个服务,我必须修改构造函数;这意味着这个类的所有单元测试都将中断。
即使是与新功能无关的测试,也至少需要重构以添加其他参数,并为该参数注入一个模拟。
这似乎是一个小问题,像resharper这样的自动化工具有帮助,但是当这样一个简单的更改导致100多个测试中断时,它确实很烦人。实际上,我见过人们为了避免更改构造函数而做一些愚蠢的事情,而不是在发生这种情况时咬紧牙关并修复所有测试。
2)不必要地传递服务实例,增加了代码复杂性。
1 2 3 4 5 | public class MyWorker { public MyWorker(int x, IMyClass service, ILogService logs) { ... } } |
如果可能的话,创建这个类的一个实例,前提是我在一个上下文中,给定的服务是可用的,并且已经被自动解析(如控制器),或者,不幸的是,通过将服务实例传递给多个助手类链。
我一直看到这样的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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框架,因为这样可以解决以下几个问题:
1 2 3 4 5 | public MyClass() { _log = Register.Get().Resolve<ILogService>(); _blah = Register.Get().Resolve<IBlahService>(); _data = Register.Get().Resolve<IDataService>(); } |
这样做有什么坏处吗?
这意味着单元测试必须具有类的"先验知识",以便在测试初始化期间为正确的类型绑定模拟,但我看不到任何其他缺点。
铌。我的例子是用C语言编写的,但是我也在其他语言中遇到了同样的问题,特别是那些不太成熟的工具支持的语言,这些都是主要的难题。
型
在我看来,所有问题的根本原因都是DI做得不对。使用构造函数DI的主要目的是清楚地说明某个类的所有依赖项。如果某件事依赖某件事,您总是有两个选择:明确这种依赖或将其隐藏在某种机制中(这种方式往往会带来比利润更大的麻烦)。
让我们看一下你的陈述:
All tests depend on the constructors.
[snip]
Problem: If I need another service in my implementation I have to modify the constructor; and that means that all of the unit tests for this class break.
号
让一个类依赖于其他服务是一个相当大的变化。如果有多个服务实现相同的功能,我会认为存在设计问题。正确的模拟和使测试满足SRP(就单元测试而言,它归结为"为每个测试用例编写一个单独的测试"),独立的应该解决这个问题。
2) Service instances get passed around unnecessarily, increasing code complexity.
号
DI最常用的用途之一是将对象创建与业务逻辑分开。在您的例子中,我们看到您真正需要的是创建一个工人,而这个工人又需要通过整个对象图注入几个依赖项。解决这个问题的最佳方法是不要在业务逻辑中执行任何
I've contemplated using the DI framework inside the constructors instead as this resolves a couple of the problems:
1
2
3
4
5 public MyClass() {
_log = Register.Get().Resolve<ILogService>();
_blah = Register.Get().Resolve<IBlahService>();
_data = Register.Get().Resolve<IDataService>();
}Is there any downside to doing this?
号
作为一个好处,您将获得使用
所以我会说DI应该做得正确(像其他任何工具一样)。解决您的问题(IMO)的方法在于了解DI和团队成员的教育。
型
对于这些问题,很容易引起构造函数注入的错误,但它们实际上是不正确实现的症状,而不仅仅是构造函数注入的缺点。
让我们分别看看每个明显的问题。
所有测试都依赖于构造函数。
这里的问题实际上是单元测试与构造函数紧密耦合。这通常可以通过一个简单的SUT工厂来解决——这个概念可以扩展到一个自动模拟容器中。
在任何情况下,当使用构造函数注入时,构造函数应该是简单的,所以没有理由直接测试它们。它们是作为您编写的行为测试的副作用出现的实现细节。
不必要地传递服务实例,增加了代码复杂性。
同意,这当然是一种代码味道,但同样,这种味道也在实现中。这不能归咎于构造函数注入。
当发生这种情况时,这是门面服务丢失的一个症状。而不是这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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(); // ... } } |
执行此操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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(); // ... } } |
号
关于建议的解决方案
提出的解决方案是一种服务定位器,它只有缺点,没有任何好处。
型
所以我以前听说过一种编码模式,在这里创建一个上下文对象,在整个代码中到处传递。上下文对象包含您想要控制的关于您的环境的任何"注入"和/或服务。
它具有gvs的一般感觉,但是您对它们有更多的控制,因为您可以将它们默认为包外的任何内容的空值,这样您就可以在包内进行测试,以访问受保护的构造器,从而允许您控制上下文对象。
例如,主类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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(); } } |
示例测试类:
1 2 3 4 5 6 7 8 9 10 11 12 | 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); } } |
。
注意,这是一个额外的工作,所以我可能只将它用于微服务和小型应用程序。大型应用程序更容易进行依赖注入。