Автор: Sergey Teplyakov

Когда речь заходит о внедрении зависимостей (Dependency Injection), то у большинства разработчиков в голове возникает образ конструктора, через который эта зависимость передается в виде интерфейса или абстрактного класса. Именно об этом виде управления зависимостей писал Боб Мартин в своей статье Dependency Inversion Principle, поэтому не удивительно, что он является самым известным.

Описание

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

image

Назначение

Разорвать жесткую связь между классом и его обязательными зависимостями.

Применимость

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

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

Даже в составе .NET Framework существует множество примером внедрения зависимостей через конструктор:// Декораторы var ms = new MemoryStream(), var bs = new BufferedStream(ms), // Стратегия сортировки var sortedArray = new SortedList<,int, string>,(
new CustomComparer()), // Класс ResourceReader принимает Stream Stream ms = new MemoryStream(), var resourceReader = new ResourceReader(ms), // BinaryReader/BinaryWriter, StreamReader/StreamWriter // также принимают Stream через конструктор var textReader = new StreamReader(ms), // Icon опять таки принимает Stream var icon = new System.Drawing.Icon(ms),

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

Применение инверсии зависимостей особенно актуально на границе модулей. Избитым, но, тем не менее, вполне актуальным примером может быть внедрение стратегии доступа к данным (интерфейса IRepository или IQueryable<,T>,) в более высокоуровневые слои приложения. Аналогичным образом можно абстрагироваться от любого набора операций, конкретная реализация которых вам не интересна.

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

Преимущества

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

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

Правило 4-х зависимостей

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

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

Это может говорить о том, что не выделены скрытые абстракции. Не нужно залипать на текущем количестве классов и интерфейсов, если класс использует по одному методу из пяти зависимостей, то в текущем дизайне явно чего-то не хватает. Подумайте, может быть стоит выделить эти пять методов в самостоятельный класс или интерфейс и передавать уже его в виде одной новой зависимости? Раз вашему классу это нужно, вполне возможно у нас присутствует сущность, о которой раньше не думали.interface IDependency1 { } interface IDependency2 { } interface IDependency3 { } interface IDependency4 { } class CustomService {
public CustomService(IDependency1 d1, IDependency2 d2,
IDependency3 d3, IDependency4 d4)
{ } }

Выделяем все 4 зависимости в новый LowLevelService:class CustomService {
public CustomService(ILowLevelService lowLevelService)
{ } }

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

ПРИМЕЧАНИЕ
Иногда советуют сгруппировать все нужные зависимости в интерфейс, который будет содержать все интерфейсы в виде свойств. Я бы не рекомендовал этот подход, поскольку мы скорее прячем проблему глубже, нежели действительно ее решаем.

Иногда бывает проще воспользоваться банальным наблюдателем и парой событий, нежели завязываться на 100500 абстракций. Ведь наблюдатель может использоваться в двух случаях: (1) когда вашему классу есть, что сказать или (2) когда вашему классу что-то нужно от внешнего окружения и вы хотите получить дополнительную информацию или запросить выполнение некоторых действий, если связность (cohesion) полученных методов очень слабая и выделить новую абстракцию не удается:interface ICustomServiceObserver {
// Добавляем нужные операции } class CustomService {
private readonly ICustomServiceObserver _observer,
public CustomService(ICustomServiceObserver observer)
{
_observer = observer,
} }

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

Необязательные зависимости

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

Обычно для передачи такого типа зависимостей принято использовать Property Injection (который мы рассмотрим в следующих постах), однако никто не запрещает сделать интерфейс нашего класса более явным и воспользоваться для этого передачей зависимости через конструктор:interface ILogger {
void Write(string message), } class DefaultLogger : ILogger { } class Service {
private readonly ILogger _logger = new DefaultLogger(),

public Service()
{ }

public Service(ILogger logger)
{
_logger = logger,
} }

Кто-то может сказать, что такое решение противоречит самой идее передаче параметров через конструктор, на что получит контр пример, поскольку такое довольно часто применяется в существующем коде, и в .NET Framework, в частности (например, в классе Icon).

Этот же подход может применяться для упрощения юнит-тестирования, когда конструктор, принимающий зависимости используется в тестах, а конструктор без параметров в продакшн коде. interface IAuthenticator {
void Authenticate(string userName), } // Находится в той же сборке, что и сервис class DefaultAuthenticator : IAuthenticator { } class Service {
private readonly IAuthenticator _authenticator,
public Service()
:
this(new DefaultAuthenticator())
{ }

// Используем в тестах internal Service(IAuthenticator authenticator)
{
_authenticator = authenticator,
} }

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

Заключение

Не зависимо от того, используете ли вы DI контейнеры или нет, Constructor Injection должен быть первым способом управления зависимостями. Его использование не только позволит сделать отношения между классами более явными, но также позволит определить проблемы с дизайном, когда количество параметров конструктора превысит определенную границу.

Цикл статей по управлению зависимостями
  1. Фреймворки, библиотеки и зависимости
  2. Управление зависимостями
  3. Наследование vs Композиция vs Агрегация
  4. DI Паттерны. Constructor Injection
  5. Property Injection и необязательные зависимости
  6. Ambient Context
  7. Service Locator

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).