Каков «правильный» способ реализации DI в .NET?

22

Я пытаюсь внедрить внедрение зависимостей в относительно большие приложения, но у меня нет опыта в этом. Я изучил концепцию и несколько доступных реализаций IoC и инжекторов зависимостей, таких как Unity и Ninject. Однако есть одна вещь, которая ускользает от меня. Как мне организовать создание экземпляра в моем приложении?

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

Будет ли это правильный подход к внедрению внедрения зависимостей в моём приложении или я должен реализовать его по другому принципу?

user3223738
источник
5
Я не думаю , что есть правильный путь, но много правильных способов, в зависимости от вашего проекта. Я бы придерживался других и предлагал внедрение в конструктор, так как вы сможете убедиться, что каждая зависимость внедрена в одну единственную точку. Кроме того, если подписи конструктора растут слишком долго, вы будете знать, что классы делают слишком много.
Пол Керчер
Трудно ответить, не зная, какой именно проект .net вы создаете. Хороший ответ для WPF может быть, например, плохим для MVC.
JMK
Хорошо организовать все регистрации зависимостей в DI-модуль для решения или для каждого проекта, и, возможно, по одному для некоторых тестов, в зависимости от того, насколько глубоко вы хотите протестировать. О да, конечно, вы должны использовать инъекцию конструктора, другие вещи для более продвинутых / сумасшедших случаев использования.
Марк Роджерс

Ответы:

30

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

Первый пункт: у Марка Симанна есть очень хорошая книга о DI в .Net

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

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

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

Миямото Акира
источник
5
Всем отличный совет; особенно первая часть: узнайте, как сделать чистый DI, а затем начните смотреть на контейнеры IoC, которые могут уменьшить объем стандартного кода, требуемого этим подходом.
Дэвид Арно
6
... или пропустить все контейнеры IoC, чтобы сохранить все преимущества статической проверки.
День
Мне нравится, что этот совет - хорошая отправная точка, прежде чем попасть в DI, и у меня есть книга Марка Симанна.
Снуп
Я могу подтвердить этот ответ. Мы успешно использовали бедный человек-DI (рукописный загрузчик) в довольно большом приложении, разделив части логики загрузчика на модули, составляющие приложение.
парик
1
Быть осторожен. Использование лямбда-инъекций может стать быстрым путем к безумию, особенно в случае повреждения конструкции, вызванного испытаниями. Я знаю. Я пошел по этому пути.
jpmc26
13

Ваш вопрос состоит из двух частей: как правильно реализовать DI и как реорганизовать большое приложение для использования DI.

На первую часть хорошо ответил @Miyamoto Akira (особенно рекомендация прочитать книгу Марка Симанна «Внедрение зависимостей в .net». Блог Marks также является хорошим бесплатным ресурсом.

Вторая часть намного сложнее.

Хорошим первым шагом было бы просто переместить все экземпляры в конструкторы классов - не вводить зависимости, а просто убедиться, что вы вызываете только newконструктор.

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

Следующей проблемой, которую вы найдете, будут классы, которые полагаются на параметры времени выполнения для построения. Обычно это можно исправить, создав простые фабрики, часто с помощью Func<param,type>инициализации их в конструкторе и вызова их в методах.

Следующим шагом будет создание интерфейсов для ваших зависимостей и добавление второго конструктора в ваши классы, кроме этих интерфейсов. Ваш конструктор без параметров обновит конкретные экземпляры и передаст их новому конструктору. Это обычно называется «B * Stard Injection» или «Dor Mans DI».

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

Конечно, вы можете пойти дальше. Если вы намереваетесь использовать контейнер IOC, то следующим шагом может быть замена всех прямых вызовов newваших конструкторов без параметров статическими вызовами контейнера IOC, по существу (ab) с использованием его в качестве локатора службы.

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

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

В конечном итоге это будет большая работа, поэтому убедитесь, что вы решили, почему вы хотите это сделать, и расставьте приоритеты в тех частях кода, которые больше всего выиграют от рефакторинга.

Стив
источник
3
Спасибо за очень сложный ответ. Вы дали мне несколько идей о том, как подойти к решению этой проблемы. Вся архитектура приложения уже создана с учетом требований IoC. Основная причина, по которой я хочу использовать DI, - это даже не модульное тестирование, а скорее бонус, а скорее возможность менять различные реализации для разных интерфейсов, определенных в ядре моего приложения, с минимальными усилиями, насколько это возможно. Рассматриваемое приложение работает в постоянно меняющейся среде, и мне часто приходится менять части приложения, чтобы использовать новые реализации в соответствии с изменениями в среде.
user3223738
1
Рад, что я мог помочь, и да, я согласен, что самое большое преимущество DI - это слабая связь и возможность легко перенастраивать, что приносит, при этом модульное тестирование является хорошим побочным эффектом.
Стив
1

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

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

Самый простой способ заявить о своей цели для рефакторинга DI состоит в том, что вы хотите удалить все экземпляры оператора 'new' из этого компонента. Они обычно делятся на две категории:

  1. Инвариантная переменная-член: это переменные, которые устанавливаются один раз (как правило, в конструкторе) и не переназначаются на время существования объекта. Для этого вы можете вставить экземпляр объекта в конструктор. Как правило, вы не несете ответственности за уничтожение этих предметов (я не хочу говорить здесь никогда, но вы действительно не должны нести эту ответственность).

  2. Переменная члена-переменной / переменная метода: это переменные, которые будут собирать мусор в определенный момент времени жизни объекта. Для этого вам нужно внедрить фабрику в ваш класс, чтобы обеспечить эти экземпляры. Вы несете ответственность за утилизацию предметов, созданных на фабрике.

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

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

«Просто» повторите вышеупомянутое для каждого компонента вашего приложения, перемещая ссылку на контейнер IoC вверх по мере продвижения, пока только основные не должны знать об этом.

Джон
источник
1
Хороший совет: начните с небольшого компонента, а не с «БОЛЬШОГО переписывания»
Даниэль Холлинрейк
0

Правильный подход заключается в использовании инжектора конструктора, если вы используете

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

тогда вы в конечном итоге с локатором службы, чем внедрение зависимости.

Низко летящий пеликан
источник
Конечно. Конструктор впрыска. Допустим, у меня есть класс, который принимает реализацию интерфейса в качестве одного из аргументов. Но мне все еще нужно создать экземпляр реализации интерфейса и передать его этому конструктору где-нибудь. Предпочтительно это должен быть какой-то централизованный кусок кода.
user3223738
2
Нет, когда вы инициализируете контейнер DI, вы должны указать реализацию интерфейсов, и контейнер DI создаст экземпляр и внедрит его в конструктор.
Низколетящий пеликан
Лично я нахожу, что инжектор конструктора чрезмерно используется. Я слишком часто видел, как внедряется 10 различных сервисов, и для вызова функции на самом деле нужен только один - почему тогда это не часть аргумента функции?
урбанистический
2
Если внедрено 10 разных сервисов, это потому, что кто-то нарушает SRP, который следует разделить на более мелкие компоненты.
Низколетящий пеликан
1
@ Фабио Вопрос в том, что это купит тебя. Я еще не видел пример, когда наличие гигантского класса, который имеет дело с дюжиной совершенно разных вещей, является хорошим дизайном. Единственное, что DI делает, это делает все эти нарушения более очевидными.
Во
0

Вы говорите, что хотите использовать это, но не говорите, почему.

DI - это не что иное, как механизм создания конкреций из интерфейсов.

Это само по себе происходит от DIP . Если ваш код уже написан в этом стиле, и у вас есть единственное место, где создаются конкреции, DI больше ничего не приносит участнику. Добавление кода структуры DI здесь просто раздувает и запутывает вашу кодовую базу.

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

NB. Очень легко накатить свой собственный, если вы хотите, а не фиксировать Ninject / StructureMap или что-то еще. Однако, если у вас разумная текучесть кадров, он может смазать колеса, чтобы использовать признанные рамки, или, по крайней мере, написать это в таком стиле, чтобы это не было слишком сложным для обучения.

Робби Ди
источник
0

На самом деле, «правильный» способ - НЕ использовать фабрику вообще, если нет абсолютно никакого другого выбора (как в модульном тестировании и некоторых моделях - для производственного кода вы НЕ используете фабрику)! Это на самом деле анти-паттерн и его следует избегать любой ценой. Весь смысл в контейнере DI состоит в том, чтобы позволить гаджету сделать всю работу за вас.

Как было сказано выше в предыдущем посте, вы хотите, чтобы ваш гаджет IoC взял на себя ответственность за создание различных зависимых объектов в вашем приложении. Это означает, что ваш DI-гаджет может создавать различные экземпляры и управлять ими. В этом весь смысл DI - ваши объекты НИКОГДА не должны знать, как создавать и / или управлять объектами, от которых они зависят. В противном случае нарушается ослабление сцепления.

Преобразование существующего приложения во все DI является огромным шагом, но, исключая очевидные трудности в этом, вы также захотите (просто чтобы сделать вашу жизнь немного проще) изучить инструмент DI, который будет автоматически выполнять большую часть ваших привязок. (ядро чего-то вроде Ninject - это "kernel.Bind<someInterface>().To<someConcreteClass>()"вызовы, которые вы делаете для сопоставления объявлений интерфейсов с теми конкретными классами, которые вы хотите использовать для реализации этих интерфейсов. Это те вызовы «Bind», которые позволяют гаджету DI перехватывать вызовы конструктора и предоставлять необходимые экземпляры зависимых объектов. Типичный конструктор (показанный здесь псевдокод) для некоторого класса может быть:

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

Обратите внимание, что нигде в этом коде не было никакого кода, который создавал / управлял / выпускал экземпляр SomeConcreteClassA или SomeOtherConcreteClassB. На самом деле ни на один конкретный класс даже не ссылались. Итак ... где случилось волшебство?

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

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

Этот небольшой фрагмент кода говорит гаджету Ninject искать конструкторы, сканировать их, искать экземпляры интерфейсов, для которых он был настроен (это вызовы «Bind»), а затем создавать и заменять экземпляр конкретного класса везде, где это необходимо. на экземпляр ссылаются.

Есть хороший инструмент, который очень хорошо дополняет Ninject, который называется Ninject.Extensions.Conventions (еще один пакет NuGet), который сделает большую часть этой работы за вас. Не для того, чтобы отвлечься от отличного опыта обучения, через который вы будете проходить, создавая его самостоятельно, но для начала, это может быть инструментом для исследования.

Если память служит, Unity (формально от Microsoft, теперь проект с открытым исходным кодом) имеет вызов метода или два, которые делают то же самое, другие инструменты имеют аналогичные помощники.

Какой бы путь вы ни выбрали, обязательно прочитайте книгу Марка Симанна для большей части вашего обучения DI, однако, следует отметить, что даже «Великие» в мире разработки программного обеспечения (такие как Марк) могут совершать явные ошибки - Марк забыл все о Ninject в своей книге, так что вот еще один ресурс, написанный только для Ninject. У меня есть это и его хорошее чтение: Освоение Ninject для инъекций зависимости

Фред
источник
0

Не существует «правильного пути», но есть несколько простых принципов, которым нужно следовать:

  • Создать корень композиции при запуске приложения
  • После того, как корень композиции был создан, выбросьте ссылку на контейнер / ядро ​​DI (или, по крайней мере, инкапсулируйте его, чтобы он не был напрямую доступен из вашего приложения)
  • Не создавайте экземпляры через «новый»
  • Передайте все необходимые зависимости как абстракцию в конструктор

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


Итак, как создавать объекты во время выполнения без «нового» и без знания контейнера DI?

В случае NInject существует расширение фабрики, которое обеспечивает создание фабрик. Конечно, созданные фабрики все еще имеют внутреннюю ссылку на ядро, но это не доступно из вашего приложения.

JanDotNet
источник