Andrey on .NET | Внедрение зависимостей. Шаблоны.

Внедрение зависимостей. Шаблоны.

Рассмотрим четыре шаблона для внедрения зависимости в объект.

1. Constructor Injection (внедрение через конструктор)

Constructor Injection это способ внедрения зависимостей, когда необходимые объекты запрашиваются классом как параметры его конструктора.

Принцип реализации

  1. Создаем в классе private readonly поле, для хранения ссылки на экземпляр объекта. Указание readonly предохраняет его от случайного изменения в дальнейшем.
  2. Запрашиваем объект как параметр конструктора.
  3. Проверяем, что полученное значение не является пустой ссылкой (null).
  4. Сохраняем в созданном выше поле для дальнейшего использования.

Плюсы

  • Очень простой для понимания и использования способ.

Минусы

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

2. Property Injection (внедрение через свойство)

При использовании Property Injection объект предоставляет свойства, в которые могут быть переданы экземпляры классов от которых он зависит.

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

Принцип реализации

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

Плюсы

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

Минусы

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

3. Method Injection (внедрение через параметр метода)

Method Injection подразумевает внедрение зависимости через параметр метода. Стоит отметить, что при каждом обращении может передаваться другой объект, в зависимости от текущего состояния приложения.

Принцип реализации

  • Один из параметров метода используется для передачи зависимости.
  • При обращении к методу проверяем, что полученное значение не является пустой ссылкой (null).
  • Используем его в процессе выполнения метода.
  • Область использования полученного объекта ограничивается данным методом.

Плюсы

  • Позволяет передавать зависимость, определяемую текущем контекстом выполнения.

Минусы

  • Ограниченная область применения.

4. Ambient Context (фоновый контекст)

Идея Ambient Context заключается в создании pubic static свойства или метода, предоставляющих экземпляр объекта определенного типа и доступного любому объекту в приложении. По смыслу такой подход применим для зависимостей, предоставляемых framework и прочими глобальными объектами приложения.

Примеры шаблона в .NET: Application.Current, Thread.CurrentCultureand, Thread.CurrentUICulture, System.Web.Http.GlobalConfiguration.Configuration, Microsoft.AspNet.SignalR.GlobalHost.DependencyResolver, OperationContext.Current и т.п.

Необходимо обратить внимание, что Ambient Context, в отличии от шаблона Service Locator, предоставляет объект одного, заранее известного типа. Это напоминает шаблон Singleton.

Принцип реализации

  • Создаем pubic static свойство или метод, возвращающие объект заданного типа.
  • Порождается экземпляр по умолчанию, даже если клиентский код может установить свой объект. Ambient Context должен быть всегда доступен.

Плюсы

  • Всегда доступен.
  • Хорошо подходит в ситуации, когда определенная зависимость необходима многим объектам приложения и эти объекты заранее неизвестны.

Минусы

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

Комментарии (4) -

Дмитрий 01.07.2013 4:18:26

способы 2 и 3 обладают еще одним, и как по мне — довольно существенным, недостатком: они размывают внешний интерфейс сущности. У доменных объектов помимо естественных для их интерфейса свойств  и параметров методов появляется куча торчащих наружу нюансов реализации: к примеру, у TaxCalculator в добавок к свойству AnnualTaxes зачем то будет выставлено наружу IDependencyOne и тп. В итоге получаем неудобство использования и явное нарушение OCP.

Еще, имхо, проверки на null для внедряемых зависимостей актуальны только для внедрения через свойства. Очевидно, что ситуация, когда объект явно требует передачи зависимости через параметр конструктора или метода, а ему приходит туда null, означает либо баг в di framework-е, либо осмысленное желание выстрелить себе в ногу у программиста. Исключение составляет лишь библиотечный код. Соответственно минус «нужны проверки на null», имхо, применим только к типу 2.

Ambient context я бы вообще считал наполовину анти-паттерном, ибо от ServiceLocator-а он мало чем отличается: та-же неявность зависимостей, только в меньшем масштабе. В итоге использовать можно только в очень малых дозах, штуки 1-3 на проект, чтобы их всегда можно было удержать в голове, и понимать где и что мочить в тестах.

@ Дмитрий: Не соглашусь про размазывание, особенно в случае с свойствами. Причина в реализации по умолчанию. А вот торчащие ненужные интерфейсы, это уже возможно ошибка проектирования.

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

Ну и касательно Ambient context - была такая мысль. Но
1) У него есть одно важное отличие от Service Locator: типа, который лежит в Ambient context известен. Зависимость тут не явная, но определенная. А вот Service Locator может выдать по сути что угодно.

2) Ambient context это крайняя мера. Но в больших библиотеках и framework без него будет сложно, т.к. нельзя заранее сказать какая реализация DI будет использоваться их пользователями.

Дмитрий 02.07.2013 2:12:00

@ Andrey:
Возможно я привел неправильный пример. Допустим, у нас есть сервис PaymentContractor, который умеет отдавать рассчитанную текущую задочлежнность перед партнером через свойство TotalDept и задолжееность на момент времени в прошлом через метоед CalculateDeptOn(Date). Чтобы эту задолженность посчитать, ему надо прочитать из базы информацию о выплатах, т.е. ему нужно получить зависимость IPaymentsRepository.
В случае инъекции через свойство, помимо логичного (с точки зрения домена) свойства TotalDept у нас появится свойство PaymentRepository, которое обладает совершенно другой семантикой. В случае инъекции через параметры, логичное  CalculateDeptOn(Date) станет CalculateDeptOn(Date, IPaymentRepository), что опять ставит в 1 ряд сущности совершенно разных уровней.

Наверное, я зря стал смешивать Property Injection и Method Injection, т.к. Method Injection при определенном подходе к проектированию работает отлично, хотя и перемещая слишком много логики в aggregation root. Я все-таки в основном критикую Property Injection, тем более что в жизни он чаще встречается.

Ambient context - согласен. Под "меньшим масштабом" я подразумивал, что 1 контекст не страшен, но если их сделать 20 разных - то беда.

@ Дмитрий: Я понял про пример, в данном случае понятно что сложно задать реализацию по умолчанию для IPaymentsRepository. Поэтому такая зависимость слабо подходит для Property Injection (IMHO наличие или отсутствие internal default веский аргумент при принятии решения). Я бы рассматривал Constructor или Method Injection.

Добавить комментарий