首页 文章

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

提问于
浏览
33

我不喜欢基于构造函数的依赖注入 .

我相信它会增加代码复杂性并降低可维护性,我想知道是否有任何可行的替代方案 .

我不是在讨论将实现与接口分离的概念,而是从接口动态解析(递归)一组对象的方法 . 我完全支持 . 但是,传统的基于构造函数的方法似乎有一些问题 .

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接口实例向下传递到多个层,除了在底层之外,它们需要注入某些东西 .

如果某个类不依赖于某个服务,那么它应该依赖于该服务 . 这个想法有一个'transient'依赖,其中一个类不使用服务但只是传递它,在我看来,是胡说八道 .

但是,我不知道更好的解决方案 .

没有这些问题,有什么能提供DI的好处吗?

我已经考虑在构造函数中使用DI框架,因为这解决了一些问题:

public MyClass() {
  _log = Register.Get().Resolve<ILogService>();
  _blah = Register.Get().Resolve<IBlahService>();
  _data = Register.Get().Resolve<IDataService>();
}

这样做有什么缺点吗?

这意味着单元测试必须具有类的“先验知识”,以便在测试初始化期间将mocks绑定到正确的类型,但我看不到任何其他缺点 .

NB . 我的例子是在c#中,但我也偶然发现了其他语言中的相同问题,特别是那些支持不太成熟的工具支持的语言是主要的问题 .

3 回答

  • 14

    在我看来,所有问题的根本原因是没有正确的DI . 使用构造函数DI的主要目的是清楚地说明某些类的所有依赖关系 . 如果某些东西取决于某些东西,你总是有两个选择:明确这种依赖性或将其隐藏在某种机制中(这种方式往往带来比利润更多的麻烦) .

    让我们来看看你的陈述:

    所有测试都取决于构造函数 . [snip]问题:如果我在实现中需要另一个服务,我必须修改构造函数;这意味着该类的所有单元测试都会中断 .

    使一个类依赖于其他服务是一个相当重大的变化 . 如果您有多个服务实现相同的功能,我会认为存在设计问题 . 正确的模拟和测试满足SRP(在单元测试方面归结为“为每个测试用例编写单独的测试”)并且独立应该解决这个问题 .

    2)服务实例不必要地传递,增加了代码复杂性 .

    DI的最常见用途之一是将对象创建与业务逻辑分开 . 在你的情况下,我们看到你真正需要的是创建一些Worker,而这又需要通过整个对象图注入的几个依赖项 . 解决此问题的最佳方法是永远不要在业务逻辑中执行任何 new . 对于这种情况,我宁愿注入一个 Worker 工厂,从 Worker 的实际创建中抽象出业务代码 .

    我已经考虑在构造函数中使用DI框架,因为这解决了一些问题:public MyClass(){
    _log = Register.Get() . 解析<ILogService>();
    _blah = Register.Get() . 解析<IBlahService>();
    _data = Register.Get() . 解析<IDataService>();
    }
    这样做有什么缺点吗?

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

    所以我会说DI应该做得对(就像任何其他工具一样) . 您的问题(IMO)的解决方案在于理解您的团队成员的DI和教育 .

  • 18

    它很容易让构造函数注入错误这些问题,但它们实际上是不正确实现的症状,而不是构造函数注入的缺点 .

    让我们分别看一下每个明显的问题 .

    All tests depend on the constructors.

    这里的问题实际上是单元测试是构造函数的 tightly coupled . 这通常可以通过简单的SUT Factory来解决 - 这个概念可以扩展到Auto-mocking Container .

    在任何情况下使用Constructor Injection时,constructors should be simple都没有理由直接测试它们 . 它们是实现细节,作为您编写的行为测试的副作用而发生 .

    Service instances get passed around unnecessarily, increasing code complexity.

    同意,这肯定是代码味道,但同样,气味在实施中 . 构造函数注入不能归咎于此 .

    当发生这种情况时,这是一个缺少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();
            // ...
    
        }
    }
    

    Regarding the proposed solution

    建议的解决方案是服务定位器和it has only disadvantages and no benefits .

  • 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);
        }
    
    }
    

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

相关问题