Возвращаемся к теме управления зависимостями, заброшенными на некоторое время.
Еще одним достаточно популярным паттерном внедрения зависимостей является 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 и рассматривал бы другие варианты только в случае необходимости.
Дополнительные ссылки
-
DI Паттерны. Property Injection
-
Ambient Context
-
Service Locator
PropertyInjection я бы использовал скорее если надо в рантайме поменять депенденси, например, писать в дебажный лог и т.д.
ОтветитьУдалитьТе же примеры, что вы использовали из Framework, на мой взгляд не должны быть примерами - это все-таки библиотека, платформа, к которой совсем другие требования по кастомизации. Иметь в обычном приложении 2 конструктора (с параметром и без) - очень редко хорошее решение.
У того же симмана проскакивали цифры - 95% constructor injection и 5% на все остальное. В общем да, если видишь, что надо использовать Property Injection - еще раз подумай. Кстати, видел как Property Injection используют когда два класса зависят друг от друга - dep1Obj = new Dep1(Dep2Obj)
ОтветитьУдалитьDep2Obj.Dep1 = dep1Obj;
class Dep2
{
IDep1 Dep1{get;set;}
}
Хотя это очень thread unsafe.
@Lonli-Lokli: насколько часто вам *нужно было* менять депенденси в рантайме? Чтобы не после создания объекта их установить, а именно во время исполнения их заменить на другие?
ОтветитьУдалитьМне, честно, нечасто.
По поводу примеров: здесь идея в том, что это общепринятый паттерн проектирования, который очень часто применяется, независимо от того, применяются контейнеры или нет. С другой стороны, в каждом приложении есть повторноиспользуемый код, требования к которому несколько иные по сравнению с остальным кодом приложения.
Кстати, мне правда интересно узнать, почему использование двух конструкторов - э то "очень редко хорошее решение". В чем его недостатки.
@eugene: да, это и правда так.
ОтветитьУдалитьКстати, циклические зависимости - это тоже, кстати, душок:)
Недавно начал читать ваш блог, интересно пишите.
ОтветитьУдалитьПара замечаний по статье.
1) Property Injection так же можно использовать обязательных зависимостей, например, в Spring свойства для внедрения зависимостей помечаются атрибутом @Autowired с булевым параметром reuired.
2) На практике повсеместно использую Property Injection, изменение свойств перед вызовом ни разу не использовал.
@Achmedzhanov Nail:
ОтветитьУдалитьУ использования обязательных зависимостей через Property Injection есть несколько проблем.
Во-первых, ты привязываешься к контейнеру (добавляя специфические атрибуты), да и использовать этот класс нормально без контейнера становится невозможно. Это может быть мелочью, но все это ограничивает использование классов в другом контексте, что может поставить окончательный крест на реюзе кода вне текущего контекста.
Во-вторых, что более важно, это очень опасный путь и вот почему. Польза Constructor Injection в том, что когда зависимостей становится слишком много, то это становится хорошо заметно по сигнатуре конструктора, и команда может принять решение по редизайну. Передавая же обязательные зависимости через свойства мы можем получить следующую картину: 2-3 зависимости передаются через конструктор, 3-4 - через свойства. Классы выглядит безобидным, но 5-7 зависимостей - это одназначно design smell, который при таком подходе совсем не очевиден.
В общем, я пока что не услышал никаких аргументов "за" использование Property Injection, кроме самого факта использования этого паттерна:)
"менять в рантайме" - подключается кастомный тул-дебаггер, который переписывает либо обрабочик ошибок на свою, либо подсовывает свой лог.
ОтветитьУдалить"два конструктора" - в случае с IoC, на мой взгляд, подход должен быть таким - все депенденси (все что приходит в конструктор) должно проходить через контейнер, то есть код
class CustomClass
{
public CustomClass(IServiceFromIoc service) {}
public CustomClass(IServiceNotFromIoc service) {}
}
и в коде где-то, где используется кастомкласс, ручками, через new() - задается тот где IServiceNotFromIoc
@Lonli-Lokli пишет...
ОтветитьУдалитьЯ предлагаю вам перечитать свой коммент, потому что я вообще не смог понять, о чем идет речь:(
(:
ОтветитьУдалитьПример класса
class CustomClass
{
public СustomClass(IServiceFromIoc service) {}
public CustomClass(IServiceNotFromIoc service) {}
}
В коде этот класс используется и через ИоК, и не через ИоК. Например, есть класс шелла, который резолвается через конструктор
class MyShell(CustomClass dependency)
Контейнер зарезолвает эту депенденси через тот конструктор, который сможет найти (public СustomClass(IServiceFromIoc service) {})
И где-то в коде будет использоваться еще один инстанс CustomClass, который будет создаваться ручками а-ля newCustomClass(new ServiceNotFromIoC)).
В случае с контейнером мне сложно представить новый проект с 2 конструкторами... Разве что это миграция старого и поддержка старых конструкторов нужна для совместимости.
Спасибо за хорошую статью, Сергей. Почитал, подумал...
ОтветитьУдалитьВспомнил хорошую статью
с прагматичным взглядом на эту тему от автора Castle Windsor Krzystof Kozmic.
Вкратце автор описывает эволюцию своих принципов для использования Constructor vs Property Injection. Последнее утверждение гласит: Use constructor for application-level dependencies and properties for infrastructure or integration dependencies, что с моей точки зрения является достаточно хорошим аргументом в пользу Property Injection.
...и несколько комментариев к комментариям:
"Во-первых, ты привязываешься к контейнеру" - полностью согласен, нужно писать код так, чтобы не было понятно какой IoC используется.
"Польза Constructor Injection в том, что когда зависимостей становится слишком много, то это становится хорошо заметно по сигнатуре конструктора..." - для выявления этих smells лучше использовать Code Analysys, или Code/Desing Review,то есть количество аргументов в конструкторе - это косвенный показатель плохого дизайна.
От себя: в большинстве случаев использую Constructor Injection, но к Property Injection отношусь как к еще одному инструменту со своими достоинствами и недостатками. Аргументы "за" и "против" очень сильно зависят от контекста применения того или иного подхода.
@Lonli-Lokli пишет...
ОтветитьУдалитьМое имхо в том, что дизайн класса не должен изменяться от того, используется ли класс с контейнером или нет.
В случае двух конструкторов, которые принимают два разных аргумента, класс становится тяжелым в использовании.
Какой вариант нужно использовать, а какой тестировать? Если объект, созданный одним конструктором ведет себя не так, как объект, созданный другим?
В общем, я понял идею, но я не понял, какие выгоды мы в этом случае получим.
@Юрий: спасибо за отличную ссылку на отличный пост!
ОтветитьУдалитьКак писал Krzystof в своем посте, его взгляд на это дело менялся несколько раз на протяжении его карьеры. На данный момент я предпочту такой подход: constructor injection для application level dependencies и ambient context - для инфраструктуры.
Основной упор Кжиштофа на передачу через свойства заключается в том, что есть зависимости, протаскивать которые заморишься, их настолько много. Из-за таких (инфраструктурных) зависимостей класс начинает пухнуть еще не начав ничего делать.
ИМХО (на данный момент), что такие зависимости можно рассматривать, как инфраструктурный контекст и использовать их как Ambient Context.
... аргументы "за" и "против" сильно зависят от контекста
Да, есть такое дело, вопрос просто в том, от каких предположений мы будем отталкиваться по умолчанию. Я обычно отталкиваюсь от принципов ООП: инвариантов, абстракций, контрактов.