Опрос одного из аргументов в пользу внедрения зависимостей: почему сложно создать граф объектов?

13

Платформы внедрения зависимостей, такие как Google Guice, дают следующую мотивацию для их использования ( источник ):

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

Создание графов объектов вручную является трудоемким (...) и затрудняет тестирование.

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

class BillingService
{
    private final CreditCardProcessor processor;
    private final TransactionLog transactionLog;

    // constructor for tests, taking all collaborators as parameters
    BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
    {
        this.processor = processor;
        this.transactionLog = transactionLog;
    }

    // constructor for production, calling the (productive) constructors of the collaborators
    public BillingService()
    {
        this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
    }

    public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
    {
        ...
    }
}

Таким образом, могут быть другие аргументы для структур внедрения зависимостей ( которые выходят за рамки этого вопроса !), Но легкое создание графов тестируемых объектов не является одним из них, не так ли?

oberlies
источник
1
Я думаю, что другим преимуществом внедрения зависимостей, которое часто игнорируется, является то, что оно заставляет вас знать, от каких объектов зависят . В объектах нет ничего волшебного, вы либо вводите явно помеченные атрибуты, либо напрямую через конструктор. Не говоря уже о том, как он делает ваш код более тестируемым.
Бенджамин Грюнбаум
Обратите внимание, что я не ищу других преимуществ внедрения зависимостей - меня интересует только аргумент, что «создание объектов сложно без внедрения зависимостей»
oberlies
Этот ответ , по-видимому, является очень хорошим примером того, почему вы хотите какой-то контейнер для графов объектов (короче говоря, вы не думаете достаточно широко, используя только 3 объекта).
Изката
@Izkata Нет, это не вопрос размера. При описанном подходе код из этого поста будет просто так new ShippingService().
Oberlies
@oberlies все еще есть; Затем вам придется копаться в 9 определениях классов, чтобы выяснить, какие классы используются для этого ShippingService, а не в одном месте
Izkata

Ответы:

12

Существует давняя, давняя дискуссия о том, как сделать инъекцию зависимости наилучшим образом.

  • Исходный фрагмент Spring создавал простой объект, а затем вводил зависимости через методы установки.

  • Но затем большое количество людей настаивали на том, что внедрение зависимостей через параметры конструктора было правильным способом сделать это.

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

Итак, ваш первый конструктор соответствует второму подходу к внедрению зависимостей. Это позволяет вам делать приятные вещи, такие как инжект-макеты для тестирования.

Но у конструктора без аргументов есть эта проблема. Поскольку он создает экземпляры классов реализации для PaypalCreditCardProcessorи DatabaseTransactionLog, он создает жесткую зависимость во время компиляции от PayPal и базы данных. Он берет на себя ответственность за правильное построение и настройку всего этого дерева зависимостей.

  • Представьте себе, что процессор PayPay представляет собой действительно сложную подсистему и дополнительно включает в себя множество вспомогательных библиотек. Создавая зависимость времени компиляции от этого класса реализации, вы создаете неразрывную ссылку на все это дерево зависимостей. Сложность вашего графа объектов только что подскочила на порядок, может быть, на два.

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

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

Если у вас есть только зависимость от интерфейса, и вы позволяете внешней фабрике создавать и внедрять зависимость, то вы отрубаете все дерево зависимостей PayPal, и сложность вашего кода останавливается на интерфейсе.

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

Например, допустим, что у PayPalProcessor было 3 зависимых объекта, а у каждой из этих зависимостей было еще два. И все эти объекты должны получить свойства из конфигурации. Код «как есть» взял бы на себя ответственность за построение всего этого, настройку свойств из конфигурации и т. Д. И т. Д. - все заботы, о которых позаботится структура DI.

Поначалу может показаться неочевидным то, от чего вы себя защищаете с помощью DI-фреймворка, но это накапливается и становится болезненно очевидным со временем. (лол, я говорю из опыта того, что пытался сделать это трудным путем)

...

На практике, даже для очень маленькой программы я нахожу, что пишу в стиле DI и разбиваю классы на пары реализации / фабрики. То есть, если я не использую DI-фреймворк, такой как Spring, я просто собираю несколько простых фабричных классов.

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

Не обязательный подход, но FWIW

...

В более общем смысле шаблон DI / interface уменьшает сложность вашего кода, выполняя две вещи:

  • абстрагирование нижестоящих зависимостей в интерфейсы

  • «поднятие» вышестоящих зависимостей из вашего кода в какой-то контейнер

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

обкрадывать
источник
Код взял бы на себя ответственность за построение всего этого - класс берет на себя ответственность только за знание того, каковы его реализации коллаборатора. Не нужно знать, что нужно этим соавторам по очереди. Так что это не так уж много.
oberlies
1
Но если бы у конструктора PayPalProcessor было 2 аргумента, откуда бы они взялись?
Роб
Это не так. Как и BillingService, в PayPalProcessor есть конструктор с нулевым аргументом, который создает коллабораторов, в которых он нуждается.
Oberlies
абстрагирование нижестоящих зависимостей в интерфейсы - пример кода делает это тоже. Поле процессора имеет тип CreditCardProcessor, а не тип реализации.
Oberlies
2
@oberlies: ваш пример кода находится между двумя стилями, стилем аргумента конструктора и конструктивным стилем с нулевыми аргументами. Заблуждение состоит в том, что конструктив ноль-аргов не всегда возможен. Прелесть DI или Factory в том, что они каким-то образом позволяют создавать объекты с помощью конструктора с нулевым аргументом.
Руонг
4
  1. Когда вы плаваете в мелком конце бассейна, все «легко и удобно». Как только вы пройдете около дюжины объектов, это больше не удобно.
  2. В вашем примере вы навсегда связали процесс выставления счетов с PayPal. Предположим, вы хотите использовать другой процессор кредитных карт? Предположим, вы хотите создать специальный процессор для кредитных карт, работающий в сети? Или вам нужно проверить обработку номера кредитной карты? Вы создали непереносимый код: «пишите один раз, используйте только один раз, потому что это зависит от конкретного графа объекта, для которого он был разработан».

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

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

BobDalgleish
источник
Когда я следую вашему аргументу, аргумент в пользу DI должен был заключаться в том, что «создание графов объектов вручную легко, но приводит к коду, который трудно поддерживать». Однако утверждалось, что «создать граф объектов вручную сложно», и я ищу только аргументы, подтверждающие это утверждение. Так что ваш пост не отвечает на мой вопрос.
Oberlies
1
Я думаю , если вы уменьшаете вопрос «создание в графе объектов ...», то это просто. Я хочу сказать, что DI решает проблему, с которой вы никогда не имеете дело только с одним графом объектов; Вы имеете дело с семьей из них.
BobDalgleish
На самом деле, просто создать один объектный граф уже сложно, если соавторы совместно используются .
Oberlies
4

Подход «создания экземпляров моих собственных сотрудников» может работать для деревьев зависимостей , но он, безусловно, не будет работать хорошо для графов зависимостей, которые являются ациклическими графами общего назначения (DAG). В DAG зависимости несколько узлов могут указывать на один и тот же узел, что означает, что два объекта используют один и тот же объект в качестве соавтора. Этот случай на самом деле не может быть построен с помощью подхода, описанного в вопросе.

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

oberlies
источник
1
Но дерево зависимостей должно быть деревом в смысле теории графов. В противном случае у вас есть цикл, который просто неудовлетворителен.
Сион,
1
@Xion Граф зависимостей должен быть ориентированным ациклическим графом. Не требуется, чтобы между двумя узлами был только один путь, как в деревьях.
Oberlies
@Xion: не обязательно. Рассмотрим, UnitOfWorkкоторый имеет единственное DbContextи несколько хранилищ. Все эти репозитории должны использовать один и тот же DbContextобъект. С предложенным OP «самоопределением» это становится невозможным.
Флэтер
1

Я не использовал Google Guice, но затратил много времени на перенос старых унаследованных приложений N-уровня в .Net на IoC-архитектуры, такие как Onion Layer, которые зависят от внедрения зависимостей для разделения вещей.

Почему инъекция зависимости?

Цель Dependency Injection на самом деле не в тестируемости, а в том, чтобы взять тесно связанные приложения и максимально ослабить связь. (Что является желательным благодаря продукту, который делает ваш код намного проще для адаптации для правильного модульного тестирования)

Почему я должен беспокоиться о связи вообще?

Сцепление или жесткая зависимость могут быть очень опасными. (Особенно в скомпилированных языках). В этих случаях у вас может быть библиотека, dll и т. Д., Которая используется очень редко и имеет проблему, которая фактически переводит все приложение в автономный режим. (Все ваше приложение умирает, потому что неважная часть имеет проблему ... это плохо ... ДЕЙСТВИТЕЛЬНО плохо) Теперь, когда вы разделяете вещи, вы действительно можете настроить свое приложение так, чтобы оно могло работать, даже если эта DLL или библиотека полностью отсутствует! Уверен, что один кусок, которому нужна эта библиотека или DLL, не будет работать, но остальные приложения будут довольны, насколько это возможно.

Зачем мне нужен Dependency Injection для правильного тестирования

На самом деле вы просто хотите получить слабосвязанный код, а Dependency Injection просто позволяет это сделать. Вы можете свободно соединять вещи без IoC, но обычно это более трудоемкий и менее адаптируемый (я уверен, что у кого-то есть исключение)

В случае, который вы дали, я думаю, что было бы намного проще просто установить внедрение зависимостей, поэтому я могу смоделировать код, который меня не интересует считать в рамках этого теста. Просто сказать вам метод «Эй , я знаю , что я сказал вам позвонить в хранилище, но вместо этого вот те данные, которые „должны“возвратные подмигивает » сейчас, потому что эти данные никогда не меняются, вы знаете, что вы тестируете только часть, которая использует эти данные, а не фактический поиск данных.

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

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

Это МОЖЕТ быть сделано без внедрения зависимостей, но обычно по мере роста вашего проекта становится все более и более громоздким делать это без внедрения зависимостей. (ВСЕГДА предполагайте, что ваш проект будет развиваться! Лучше иметь ненужную практику полезных навыков, чем находить проект, который быстро набирает обороты и требует серьезного рефакторинга и реинжиниринга после того, как что-то уже взлетело.)

RualStorge
источник
1
Написание тестов, использующих большую часть, но не весь граф зависимостей, на самом деле является хорошим аргументом для DI.
oberlies
1
Стоит также отметить, что с помощью DI вы можете легко программно поменять целые куски кода. Я видел случаи, когда система очищала и повторно вводила интерфейс с совершенно другим классом, чтобы реагировать на сбои и проблемы производительности удаленными сервисами. Возможно, был лучший способ справиться с этим, но он работал на удивление хорошо.
RualStorge
0

Как я уже упоминал в другом ответе , проблема заключается в том, что вы хотите, чтобы класс Aзависел от какого-то классаB без жесткого программирования, какой классB используется в исходном коде A. Это невозможно в Java и C #, потому что единственный способ импортировать класс это ссылаться на него глобально уникальным именем.

Используя интерфейс, вы можете обойти жестко запрограммированную зависимость класса, но вам все еще нужно получить экземпляр интерфейса, и вы не можете вызывать конструкторы, или вы вернулись на круги своя. Итак, теперь код в противном случае это может привести к возникновению его зависимостей, и это перекладывает эту ответственность на кого-то другого. И его зависимости делают то же самое. Так что теперь каждый раз, когда вам нужен экземпляр класса, вы в конечном итоге создаете все дерево зависимостей вручную, тогда как в случае, когда класс A напрямую зависит от B, вы можете просто вызвать new A()этот вызов конструктора new B()и т. Д.

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

Doval
источник
0

Я думаю, что это большое недоразумение здесь.

Guice - это структура внедрения зависимостей . Это делает DI автоматически . Суть, которую они указали в приведенной вами выдержке, заключается в том, что Guice избавился от необходимости вручную создавать «тестируемый конструктор», который вы представили в своем примере. Это не имеет абсолютно никакого отношения к внедрению зависимости.

Этот конструктор:

BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
    this.processor = processor;
    this.transactionLog = transactionLog;
}

уже использует внедрение зависимостей. Вы просто сказали, что использовать DI легко.

Проблема, которую решает Guice, состоит в том, что для использования этого конструктора у вас должен быть где-то код конструктора графа объектов , который вручную передает уже созданные объекты в качестве аргументов этого конструктора. Guice позволяет вам иметь единственное место, где вы можете настроить, какие классы реальной реализации соответствуют тем CreditCardProcessorи TransactionLogинтерфейсам. После этой конфигурации каждый раз, когда вы создаете BillingServiceс помощью Guice, эти классы автоматически передаются в конструктор.

Это то, что делает структура внедрения зависимостей . Но сам конструктор, который вы представили, уже является реализацией принципа внедрения зависимостей . Контейнеры IoC и DI-фреймворки - это средства для автоматизации соответствующих принципов, но ничто не мешает вам делать все вручную, в этом весь смысл.

hijarian
источник