Главная страница   /   6.4. Обсуждение феномена Constructor Over-injection (Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Марк Симан

6.4. Обсуждение феномена Constructor Over-injection

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

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

Распознание и решение проблемы Constructor Over-injection

Когда список параметров конструктора становится слишком большим, мы называем это явление Constructor Over-injection и считают его плохо пахнущим кодом. Этот плохо пахнущий код не появляется, но усугубляется в результате использования DI. Хотя нашей начальной реакцией может быть то, что нам не нравится внедрение в конструктор из-за Constructor Over-injection, мы должны быть благодарны, что общая проблема проектирования открывается нам.

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

Constructor Over-injection как сигнал

Хотя внедрение в конструктор легко реализовать и использовать, оно доставляет неудобство, когда конструкторы начинают выглядеть так:

public MyClass(IUnitOfWorkFactory uowFactory,
	CurrencyProvider currencyProvider,
	IFooPolicy fooPolicy,
	IBarService barService,
	ICoffeeMaker coffeeMaker,
	IKitchenSink kitchenSink)

Я не могу сказать, что я кого-то обвиняю, если он не любит такой конструктор, но я не виню внедрение в конструктор. Я могу согласиться, что конструктор с шестью параметрами сигнализирует о плохо пахнущем коде, но это свидетельствует о нарушении Принципа единственной обязанности, а не о проблемах, связанных с DI.

Совет

Внедрение в конструктор позволяет легко определить нарушения принципа единственной обязанности.

Вместо того чтобы чувствовать неловкость из-за Constructor Over-injection, мы должны принять его как удачный побочный эффект внедрения в конструктор. Это сигнал, который предупреждает нас всякий раз, когда класс берет на себя слишком большую ответственность.

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

То, как мы проводим рефакторинг определенного класса, который слишком сильно вырос, зависит от конкретных обстоятельств: на месте ли объектная модель, домен, бизнес-логика и так далее. Разделение Божественного класса (God Class) на более мелкие, более сфокусированные классы в соответствии с известными паттернами проектирования – это всегда хороший ход.

Тем не менее, бывают случаи, когда бизнес требования обязывают нас делать много разных вещей в одно и то же время. Это часто случается в пограничной области приложения. Подумайте о крупнозернистой операции веб сервиса, которая запускает много бизнес событий. Один из способов моделирования таких операций заключается в сокрытии множества зависимостей за Фасадными сервисами (Facade Services).

Рефакторинг по направлению к Фасадным сервисам

Есть много способов, как мы можем разработать и осуществить необходимые операции, так чтобы они не нарушали принцип единственной обязанности. В главе 9 мы обсудим, как паттерн проектирования Декоратор (Decorator) может помочь нам со стеком Cross-Cutting Concerns, вместо внедрения их в потребляющие элементы в виде сервисов. Это может устранить лишнее число аргументов конструктора.

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

На рисунке 6-11 показано, как мы можем провести рефакторинг ключевых отношений по направлению к фасадным сервисам.

Рисунок 6-11: На самой верхней диаграмме видно, что у потребителя есть пять зависимостей, которые являются сильным признаком того, что нарушен принцип единственной обязанности. Тем не менее, если роль потребителя заключается в том, чтобы организовать эти пять зависимостей, мы не можем какую-либо выбросить. Вместо этого мы можем ввести фасадные сервисы, которые организуют части отношений. В нижней части диаграммы у потребителя есть только две зависимости, а у фасадов две и три зависимости.

Рефакторинг к фасадным сервисам – это просто трюк, чтобы избавиться от слишком большого числа зависимостей. Ключом является определение естественных кластеров взаимодействия. На рисунке 6-11 показано, что зависимости AC формируют естественный кластер взаимодействия, и также делают D и Е.

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

Примечание

Фасадные сервисы являются абстрактными фасадами, как следует из названия.

Фасадные сервисы связаны с Parameter Objects, но вместо объединения и раскрытия компонентов, фасадный сервис раскрывает только инкапсулированное поведение, скрывая компоненты.

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

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

Давайте рассмотрим пример.

Пример: рефакторинг приема заказов

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

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

Таблица 6-2: Когда подсистема заказов получает новый заказ, она должна выполнить различные операции
Действие Требуемые зависимости
Сохранить заказ OrderRepository
Отправить имейл о заказе покупателю IMessageService
Сообщить системе учета сумму счета IBillingSystem
Выбрать лучшие склады, чтобы подобрать и отправить заказ на основе товаров в заказе и близости к адресу доставки ILocationService, IInventoryManagement
Запросить выбранные склады подобрать и отправить весь заказ или часть его IInventoryManagement

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

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

Слишком много детальных зависимостей

Если вы позволите OrderService непосредственно потреблять все пять зависимостей, структура будет такой, как показано на рисунке 6-12.

Рисунок 6-12: У OrderService есть пять зависимостей, и это сигнализирует о том, что нарушен принцип единственной обязанности.

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

Вы можете решить эту проблему, переделав OrderService.

Рефакторинг по направлению к фасадным сервисам

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

Если вы подумаете об этом чуть больше, то оказывается, что ILocationService является деталью реализации уведомления соответствующих складов о заказе. Все взаимодействие может быть скрыто за интерфейсом IOrderFulfillment, как показано на рисунке 6-13. Интересно, что выполнение заказов звучит очень похоже на концепцию; есть вероятность, что вы только что обнаружили неявную доменную концепцию и сделали ее явной.

Рисунок 6-13: Взаимодействие между IInventoryManagement и ILocationService осуществлено в классе LocationOrderFulfillment, который реализует интерфейс IOrderFulfillment. Потребители интерфейса IOrderFulfillment понятия не имеют, что у реализации есть две зависимости.

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

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

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

public interface INotificationService
{
	void OrderAdded(Order order);
}

Совет

Паттерн проектирования Domain Event (доменное событие) может быть хорошей альтернативой для данного сценария.

Каждое уведомление внешней системы может быть реализовано с помощью этого интерфейса. Вы можете даже рассмотреть обертывание OrderRepository в INotificationService, но вполне вероятно, что классу OrderService будет необходим доступ к другим методам OrderRepository для реализации других функций. Рисунок 6-14 показывает, как вы реализуете другие уведомления при помощи INotificationService.

Рисунок 6-14: Каждое уведомление внешней системы может быть спрятано за INotificationService: даже новый интерфейс IOrderFulfillment, который вы только ввели.

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

Да. Поскольку все три уведомления реализуют один интерфейс, вы можете обернуть их в Компоновщик (Composite). Это еще одна реализация INotificationService, которая обрабатывает коллекцию экземпляров INotificationService и вызывает метод OrderAdded для них всех.

С концептуальной точки зрения это также имеет смысл, поскольку с высокоуровневого представления вы не заботитесь о деталях того, как OrderService уведомляет другие системы. Тем не менее, вас волнует, что он делает. Рисунок 6-15 показывает конечные зависимости OrderService.

Рисунок 6-15: Окончательный OrderService с зависимостями после рефакторинга. Вы оставляете OrderRepository как отдельную зависимость, потому что вам нужны его дополнительные методы для реализации других функций OrderService. Все другие уведомления скрыты за интерфейсом INotificationService. Во время выполнения вы используете CompositeNotificationService, который содержит оставшиеся три уведомления.

Это уменьшает OrderService до двух зависимостей, что является намного более разумным числом. Функциональность не изменилась, что делает это истинным рефакторингом. С другой стороны, изменился концептуальный уровень OrderService. В его обязанности сейчас входит: получить заказ, сохранить его и уведомить другие системы. Детали того, какие системы извещены и как это реализовано, были вытеснены на более детальный уровень.

Даже если вы везде последовательно использовали внедрение в конструктор, ни один конструктор класса не должен требовать более двух параметров (CompositeNotificationService принимает IEnumerable<INotificationService> как один аргумент).

Constructor Over-injection – это не проблема, связанная с DI в целом или, в частности, с внедрением в конструктор. Скорее, это сигнал о том, что класс имеет слишком много обязанностей. В этом классе плохо пахнет код, а не внедрение в конструктор; и как всегда, мы должны внимательно отнестись к плохо пахнущему коду и улучшить его.

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

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