Жизнеспособная альтернатива внедрению зависимости? - PullRequest
33 голосов
/ 06 февраля 2012

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

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

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

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

После широкого использования DI в проекте MVC 3 C # надв прошлом году наш код был полон таких вещей:

public interface IMyClass {
   ...
}

public class MyClass : IMyClass { 

  public MyClass(ILogService log, IDataService data, IBlahService blah) {
    _log = log;
    _blah = blah;
    _data = data;
  }

  ...
}

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

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

Это кажется небольшой проблемой и автоматизированными инструментами, такими как resharper help, но, безусловно, раздражает, когда простое изменение, подобное этому, приводит к сбою более 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>();
}

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

Это означает, что модульные тесты должны иметь «предварительные знания» о классе, чтобы связывать макеты для правильных типов во время теста инициализации, но я не вижу никаких других недостатков.

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

Ответы [ 3 ]

18 голосов
/ 06 февраля 2012

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

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

[snip]

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

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

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

Я уже рассматривал использование DI-фреймворка внутри конструкторов, поскольку это решает пару проблем:

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

Есть ли у этого недостаток?

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

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

15 голосов
/ 06 февраля 2012

Заманчиво винить Constructor Injection в этих проблемах, но на самом деле они являются симптомами неправильной реализации, а не недостатками Constructor Injection.

Давайте рассмотрим каждую кажущуюся проблему отдельно.* Все тесты зависят от конструкторов.

Проблема здесь в том, что модульные тесты тесно связаны с конструкторами.Часто это можно исправить с помощью простой SUT Factory - концепции, которая может быть расширена до Контейнера с автоматической фиксацией .

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

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

Согласен, этоконечно запах кода, но опять же запах в реализации.В этом нельзя винить 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();
        // ...

    }
}

Относительно предложенного решения

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

0 голосов
/ 06 декабря 2018

Итак, я слышал о шаблоне кодирования до того, как вы создадите объект 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);
    }

}

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...