Продолжаем разбираться с внедрениями зависимостей в .Net, так как данная тема является одним из обязательных к изучению для написания качественного, гибкого к изменениям и тестируемого кода. Сегодня разберем внедрение через свойство или Property Injection. Итак, поехали!
Назначение
Разорвать жесткую связь между классом и его необязательными зависимостями.
Описание
Как можно разрешить внедрение зависимостей как опцию в классе, если имеется подходящее локальное умолчание?
Использованием записываемого свойства, что позволяет вызывающей стороне устанавливать его значение, если она хочет заменить поведение, применяемое по умолчанию.
Класс, использующий зависимость, должен иметь записываемое свойство с модификатором public: тип этого свойства должен соответствовать типу зависимости.
1 2 3 4 | public class SomeClass { public ISomeInterface Dependency { get; set; } } |
Здесь SomeClass зависит от ISomeInterface. Клиенты могут передавать реализации интерфейса ISomeInterface через свойство Dependency. Обратите внимание, что в противоположность внедрению конструктора, вы не можете отметить поле свойства Dependency как «только для чтения» (Read Only), так как вызывающей стороне позволено изменять значение этого свойства в любой момент жизненного цикла класса SomeClass.
Прочие члены зависимого класса могут использовать инжектированную зависимость для выполнения своих функций, например:
1 2 3 4 | public string DoSomething(string message) { return this.Dependency.DoStuff(message); } |
Однако такая реализация является ненадежной, поскольку свойство Dependency не гарантирует возврата экземпляра ISomeInterface. Например, код, показанный ниже, будет генерировать NullReferenceException, так как значение свойства Dependency есть null:
1 2 | var mc = new SomeClass(); mc.DoSomething("Hello world!"); |
Данная проблема может быть устранена установкой в конструкторе экземпляра зависимости по умолчанию для свойства, скомбинированной с добавлением проверки на null в метод — установщик свойства.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class SomeClass { private ISomeInterface _dependency; public SomeClass() { _dependency = new DefaultSomeInterface(); } public ISomeInterface Dependency { get => _dependency; set => _dependency = value ?? throw new ArgumentNullException(nameof(value)); } } |
Трудность возникает, если клиентам будет позволено менять значение зависимости в течение жизненного цикла класса.
Что должно произойти, если клиент попытается изменить значение зависимости в течение жизненного цикла класса?
Следствием этого может быть противоречивое или неожиданное поведение класса, поэтому лучше защититься от такого поворота событий.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class SomeClass { private ISomeInterface _dependency; public ISomeInterface Dependency { get => _dependency ?? (_dependency = new DefaultDependency()); set { //Разрешается только 1 раз определять зависимость if (_dependency != null) throw new InvalidOperationException(nameof(value)); _dependency = value ?? throw new ArgumentNullException(nameof(value)); } } } |
Создание DefaultDependency может быть отложено до момента, пока свойство не будет запрошено в первый раз. В таком случае произойдет отложенная инициализация. Обратите внимание, что локальное умолчание назначается через сеттер с модификатором public, что обеспечивает выполнение всех защитных блоков. Первый блок защиты гарантирует, что устанавливаемая зависимость не null (можем при использовании словить NRE). Следующий защитный блок отвечает за то, чтобы зависимость была установлена только один раз.
Вы можете также заметить, что, зависимость будет блокирована после того, как свойство будет прочитано. Это сделано для защиты клиентов от ситуаций, когда зависимость позднее изменяется без каких-либо извещений, в то время как клиент думает, что зависимость осталась прежняя.
Когда следует применять внедрение свойства
Внедрение свойства следует применять только в случае, когда для разрабатываемого класса имеется подходящее локальное умолчание, но при этом вы хотели бы оставить вызывающей стороне возможность использовать другую реализацию типа зависимости. Внедрение свойства лучше всего применять, если зависимость опциональна. Следует считать, что свойства являются опциональными, ведь легко забыть присвоить им значение, и компилятор никак не отреагирует на это.
Может показаться заманчивым задать эту реализацию по умолчанию для данного класса во время разработки. Однако если такое заблаговременное умолчание реализуется в другой сборке (Assembly), использование ее таким способом неизбежно вызовет создание неизменяемой ссылки на нее, что сведет на нет многие преимущества слабого связывания.
Внедрение свойств — это лишь один из многих возможных способов использования принципа открытости/закрытости.
Предостережения
- Использование Property Injection для обязательных зависимостей.
Это одна из самых распространенных ошибок использования этого паттерна. Если нашему классу обязательно нужна некоторая зависимость, то ее следует передавать через конструктор, чтобы сразу после создания объекта он был в валидном состоянии. - Использование Foreign Default вместо Local Default.
Одной из опасностей использования реализации зависимостей по умолчанию является использование конкретной зависимости, расположенной в сборке, о которой наш сервис знать не должен (Foreign Default). Если таких сервисов будет много, то мы получим десятки лишних физических связей, которые усложнят понимание и сопровождение. Реализация по умолчанию должна находиться в той же сборке(Local Default). - Сложность.
Проблема использования Property Injection для обязательных зависимостей заключается в том, что это очень сильно увеличивает сложность класса. Класс с тремя полями, каждое из которых может быть null приводит к 8 разным комбинациям состояния объекта. Попытка проверить состояние в теле каждого открытого метода приводит к ненужному скачку сложности. - Привязанность к контейнеру.
В большинстве случаев мы должны использовать контейнер в минимальном количестве мест. Использование Constructor Injection в целом, позволяет этого добиться, поскольку его использование не привязывает ваш класс к какому-то конкретному контейнеру. Однако ситуация меняется при использовании Property Injection. Большинство контейнеров содержат набор специализированных атрибутов для управлением зависимостями через свойства (SetterAttribute для StructureMap, Dependency для Unity, DoNotWire для Castle Windsor и т.д.). Такая жесткая связь не позволит вам «передумать» и перейти на другой контейнер или вообще отказаться от их использования. - Write-only свойства
Далеко не всегда мы хотим выставлять наружу свойство, возвращающее зависимость. В этом случае нам придется либо делать свойство только для записи (set-only property), что противоречит общепринятым принципам проектирования на платформе .NET или использовать метод вместо свойства (использовать Setter Method Injection).1 2 3 4 5 6 7 8 9
public class SomeClass { private ISomeInterface _dependency; public void SetDependency(ISomeInterface dependency) { _dependency = dependency; } }
Альтернативы
Если у нас есть класс, который содержит необязательную зависимость, то можно воспользоваться старым подходом с двумя конструкторами:
1 2 3 4 5 6 7 8 9 10 11 12 | public class SomeClass { private ISomeInterface _dependency; public SomeClass() : this(new DefaultSomeInterface()) { } public SomeClass(ISomeInterface dependency) { _dependency = dependency; } } |
Заключение
Внедрение через свойство (Property Injection) идеально подходит для необязательных зависимостей. Они вполне подойдут для стратегий с реализацией по умолчанию, но все равно, я бы рекомендовал использовать Constructor Injection и рассматривал бы другие варианты только в случае необходимости.