依赖注入的可行替代方案?

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最常用的用途之一是将对象创建与业务逻辑分开。在您的例子中,我们看到您真正需要的是创建一个工人,而这个工人又需要通过整个对象图注入几个依赖项。解决这个问题的最佳方法是不要在业务逻辑中执行任何new。对于这种情况,我更愿意注入一个工人工厂,从工人的实际创建中提取业务代码。

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?

作为一个好处,您将获得使用Singleton模式的所有缺点(不稳定的代码和应用程序的巨大状态空间)。

所以我会说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);
    }

}

注意,这是一个额外的工作,所以我可能只将它用于微服务和小型应用程序。大型应用程序更容易进行依赖注入。