Подтвердить что ты не робот

Жизнеспособная альтернатива инъекции зависимостей?

Мне не нравится инъекция зависимостей на основе конструктора.

Я считаю, что это увеличивает сложность кода и снижает ремонтопригодность, и я хотел бы знать, существуют ли какие-либо жизнеспособные альтернативы.

Я не говорю о концепции ветки реализации от интерфейса и возможности динамического решения (рекурсивно) набора объектов из интерфейса. Я полностью поддерживаю это. Тем не менее, традиционный конструктор, основанный на этом, кажется, имеет несколько проблем.

1) Все тесты зависят от конструкторов.

После интенсивного использования DI в проекте MVC 3 С# за последний год я нахожу наш код таким, как это:

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 для правильных типов во время тестирования init, но я не вижу других недостатков.

NB. Мои примеры находятся в С#, но я тоже наткнулся на те же проблемы на других языках, и особенно языки с менее зрелой инструментальной поддержкой - это главные головные боли.

4b9b3361

Ответ 1

По моему мнению, первопричиной всех проблем является не право DI. Основной целью использования конструктора DI является четкое определение всех зависимостей некоторого класса. Если что-то зависит от чего-то, у вас всегда есть два варианта: сделать эту зависимость явной или скрывать ее в каком-то механизме (этот способ, как правило, приносит больше головных болей, чем прибыль).

Переходите к своим утверждениям:

Все тесты зависят от конструкторов.

[надрез]

Проблема: если мне нужна другая служба в моей реализации, мне нужно изменить конструктор; и это означает, что все модульные тесты для этого класса прерываются.

Создание класса зависит от какой-либо другой услуги, это довольно серьезное изменение. Если у вас есть несколько служб, реализующих одну и ту же функциональность, я бы сказал, что есть проблема с дизайном. Правильное издевательство и выполнение тестов удовлетворяют SRP (что в терминах модульных тестов сводится к "Написание отдельного теста на каждый тестовый пример" ), и независимый должен решить эту проблему.

2) Служебные экземпляры передаются без необходимости, увеличивая сложность кода.

Одно из самых общих применений DI - это отделить создание объекта от бизнес-логики. В вашем случае мы видим, что вам действительно нужно создать некоторого Рабочего, который, в свою очередь, нуждается в нескольких зависимостях, которые вводятся через весь граф объекта. Лучший подход для решения этих проблем - никогда не делать new в бизнес-логике. Для таких случаев я бы предпочел ввести рабочий factory абстрактный бизнес-код из фактического создания рабочего.

Я предполагал использовать структуру DI внутри конструкторов, так как это устраняет пару проблем:

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

Есть ли недостаток в этом?

В качестве преимущества вы получите все недостатки использования шаблона Singleton (непроверяемый код и огромное пространство состояний вашего приложения).

Итак, я бы сказал, что DI должен быть сделан правильно (как и любой другой инструмент). Решение ваших проблем (IMO) заключается в понимании DI и обучении членов вашей команды.

Ответ 2

Это заставляет задуматься о конструкторской инъекции для этих проблем, но они на самом деле являются симптомами неправильной реализации больше, чем недостатки инжекции конструктора.

Посмотрите на каждую кажущуюся проблему индивидуально.

Все тесты зависят от конструкторов.

Проблема в том, что модульные тесты тесно связаны с конструкторами. Это часто можно устранить простым SUT Factory - концепцией, которая может быть расширена в Авто-издевательский контейнер.

В любом случае при использовании Constructor Injection конструкторы 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();
        // ...

    }
}

Что касается предлагаемого решения

Предлагаемое решение - это локатор сервисов, а имеет только недостатки и отсутствие преимуществ.

Ответ 3

Поэтому я слышал о шаблоне кодирования до того места, где вы создаете объект Context, который передается по всему вашему коду. Объект Context содержит любые "инъекции" и/или услуги, которые вы хотели бы контролировать в своей среде.

У этого есть общее чувство GV, но у вас есть намного больше контроля над ними, потому что вы можете по умолчанию их игнорировать для чего-либо вне пакета, поэтому вы можете иметь тесты внутри пакета, которые могут получить доступ к защищенному конструктору, что позволит вам управлять объектом 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 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);
    }

}

Обратите внимание, что это немного дополнительная работа, поэтому я, вероятно, буду использовать ее только для микросервисов и небольших приложений. Большим приложениям было бы проще просто делать инъекции зависимостей.