Автор: Sergey Teplyakov

Возвращаемся к теме управления зависимостями, заброшенными на некоторое время.

Еще одним достаточно популярным паттерном внедрения зависимостей является Property Injection, который заключается в передаче нужных зависимостей через setter свойства. Все современные DI-контейнеры в той или иной мере поддерживают этот паттерн, что делает его использование достаточно простым. Я рекомендую быть осторожным с этим паттерном, поскольку с точки дизайна передача зависимостей через свойства усложняет использование, понимание и поддержку.

Но давайте обо всем по порядку.

Описание

Суть паттерна заключается в том, что необязательная зависимость, требуемая некоторому классу, может быть переопределена вызывающим кодом путем установки ее через свойство.// Dependency.dll public interface IDependency { } // CustomService.dll (!) internal class DefaultDependency : IDependency { } // CustomService.dll public class CustomService {
public CustomService()
{
Dependency =
new DefaultDependency(),
}
public IDependency Dependency { get, set, } }

Назначение

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

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

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

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

ПРИМЕЧАНИЕ
Хорошим примером необязательной зависимости является интерфейс IComparer of T для класса SortedList of T. И хотя в этом случае существует реализация зависимости по умолчанию (DefaultComparer) передача ее через свойство выглядит весьма подозрительным, поскольку comparer не должен изменяться с течением времени жизни объекта SortedList.

Использование этого паттерна возможно лишь в том случае, когда класс сервиса (CustomService) знает о реализации зависимости по умолчанию (DefaultDependency), поскольку она находится в сборке сервиса или в сборке, где зависимость объявлена (реализации называют Local Default). Подобную технику нельзя использовать, когда реализация по умолчанию располагается в сборке, о которой классу сервиса ничего не известно (так называемые Foreign Default Dependencies). В таком случае использование зависимости приведет к более тесной связи между классом сервиса конкретной реализацией зависимости.

Известные применения

Существует десятки примеров использования Property Injection в .NET Framework, просто далеко не всегда мы обращаем внимание, что сталкиваемся с известным паттерном. Вот несколько примеров:

1. Свойство DataContext для привязки данных в WPF:var view = new ErrorMessageWindow() { DataContext = viewModel },

2. Суррогаты для кастомной сериализации объектов:var formatter = new BinaryFormatter(), var ss = new SurrogateSelector(), // Добавляем в SurrogateSelector нужный суррогат // Передаем SurrogateSelector через свойство formatter.SurrogateSelector = ss,

3. Многие точки расширения в WCF:public class CustomEndpointBehavior : IEndpointBehavior {
public void ApplyDispatchBehavior(ServiceEndpoint endpoint,
EndpointDispatcher endpointDispatcher)
{
// Передаем CustomOperationSelector через свойство endpointDispatcher.DispatchRuntime.OperationSelector =
new CustomOperationSelector(),
}
// Остальные методы опущены для простоты }

4. Свойства SelectCommand, InsertCommand, DeleteCommand и UpdateCommand интерфейса IDbDataAdapter:var command = new OleDbCommand(query, connection), var adapter = new OleDbDataAdapter(), // Передаем SelectCommand через свойство adapter.SelectCommand = command,

Предостережения

1. Использование Property Injection для обязательных зависимостей.

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

2. Использование Foreign Default вместо Local Default.

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

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

3. Сложность.

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

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

Еще одним аспектом, который нужно учитывать является то, что далеко не каждая зависимость, передаваемая через свойства может изменяться во время жизни объекта. Если это так (а очень часто мы не можем поменять стратегию сортировку после создания объекта), то логика setter-а еще усложняется, поскольку необходимо реализовать write-once паттерн.public IDependency Dependency {
get { return _dependency, }
set
{
if (value == null)
throw new ArgumentNullException('value'),
if (_dependencyWasChanged)
throw new InvalidOperationException(
'You can set dependency only once.'),
_dependency =
value,
_dependencyWasChanged =
true, } }

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

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

4. Auto-wiring

Некоторые контейнеры (такие как Castle Windsor) автоматически устанавливают все зависимости через свойства, доступные для записи. Такая неявность может привести к нежелательным последствиям, поскольку вносит дополнительную связанность между вашим классом и местом инициализации контейнера.

5. Привязанность к контейнеру

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

Однако ситуация меняется при использовании Property Injection. Большинство контейнеров содержат набор специализированных атрибутов для управлением зависимостями через свойства (SetterAttribute для StructureMap, Dependency для Unity, DoNotWire для Castle Windsor и т.д.). Такая жесткая связь не позволит вам передумать и перейти на другой контейнер или вообще отказаться от их использования.

6. Write-only свойства

Далеко не всегда мы хотим выставлять наружу свойство, возвращающее зависимость. В этом случае нам придется либо делать свойство только для записи (set-only property), что противоречит общепринятым принципам проектирования на платформе .NET или использовать метод вместо свойства (использовать так называемый Setter Method Injection).

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

Альтернативы

Если у нас есть класс, который содержит необязательную зависимость, то я бы предложил использовать старый добрый подход с двумя конструкторами:public class CustomService {
private readonly IDependency _dependency,
public CustomService()
:
this(new DefaultDependency())
{ }
public CustomService(IDependency dependency)
{
_dependency = dependency,
} }

Этот подход не всеми признается, например, Марк Сииман, автор книги Dependency Injection in .NET называет его Bastard Injection и считает одним из анти-паттернов, не смертельным, но все же. Я не вижу особой проблемы в этом подходе, тем более, что он интенсивно используется в .NET Framework (по сути, он подходит везде, когда класс принимает в качестве зависимости стратегию с реализацией по умолчанию).

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

Заключение

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

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

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

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

You have no rights to post comments

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

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