- PVSM.RU - https://www.pvsm.ru -
Давайте разберемся с внедрением зависимостей в .Net, так как данная тема является одним из обязательных к изучению для написания качественного, гибкого к изменениям и тестируемого кода. Начнем мы с самих необходимых и базовых паттернов внедрения зависимостей — внедрение через конструктор и через свойство. Итак, поехали!
Разорвать жесткую связь между классом и его обязательными зависимостями.
Суть паттерна сводится к тому, что все зависимости, требуемые некоторому классу передаются ему в качестве параметров конструктора, представленных в виде интерфейсов или абстрактных классов.
Как можно гарантировать, что требуемая зависимость будет всегда доступна разрабатываемому классу?
Это обеспечивается, если все вызывающие классы будут передавать зависимость как параметр конструктора.
Класс, требующий зависимость, должен иметь конструктор с модификатором доступа public (общедоступный), который получает экземпляр требуемой зависимости в качестве аргумента конструктора:
private readonly IFoo _foo;
public Foo(IFoo foo)
{
if (foo == null)
throw new ArgumentNullException(nameof(foo));
_foo = foo;
}
Зависимость является обязательным аргументом конструктора. Код любого клиента, который не предоставляет экземпляра зависимости, не может компилироваться. Однако поскольку как интерфейс, так и абстрактный класс являются ссылочными типами, вызывающий код может передать в аргумент специальное значение null, что делает приложение компилируемым. Поэтому в классе делается проверка на null, которая защищает класс от такого некорректного использования. Поскольку совместная работа компилятора и блока защиты(проверка на null) гарантирует, что аргумент конструктора является корректным (если не возникла исключительная ситуация (Exception)), конструктор может просто сохранить зависимость для будущего использования, не выясняя детали реальной реализации.
Хорошей практикой является объявлять поле, хранящее значение зависимости, как «только для чтения» (Read-Only). Так мы гарантируем, что выполняется, причем только однажды, логика инициализации в конструкторе: поле не может быть модифицировано. Это не нужно для реализации внедрения зависимостей, но таким образом код защищается от случайных модификаций поля (например, от установки его значения в null) в каком-то другом месте кода класса.
Внедрение через конструктор должно по умолчанию использоваться с внедрением зависимостей. Оно реализует наиболее популярный сценарий, когда классу нужны одна или более зависимостей, а в наличии не имеется подходящих локальных умолчаний.
Рассмотрим наиболее лучшие советы и практики по использования внедрения через конструктор:
Достоинства | Недостатки |
Внедрение гарантировано | В некоторых фреймворках сложно задействовать внедрение через конструктор |
Простота реализации | Требование немедленной инициализации всего графа зависимости (*) |
Обеспечение четкого контракта между классом и его клиентами (проще думать о текущем классе, не задумываясь о том, откуда берутся зависимости у более высокоуровневого класса) | - |
Сложность класса становится очевидно | - |
(*)Очевидным недостатком внедрения конструктора является требование немедленной инициализации всего графа зависимости — зачастую уже при запуске приложения. Тем не менее, хотя и кажется, что этот недостаток снижает эффективность системы, на практике он редко становится проблемой. Даже для сложных графов объектов создание экземпляра объекта — это действие, которое .NET фреймворк выполняет чрезвычайно быстро. В очень редких случаях эта проблема может оказаться действительно серьезной. Тогда воспользуемся параметром жизненного цикла, называемый Delayed (отложенный), который вполне подходит для решения этой проблемы.
Потенциальной проблемой использования конструктора для передачи зависимостей может быть чрезмерное увеличение параметров конструктора. Здесь [1]можно подробнее прочитать.
Другой причиной большого количества параметров конструктора может быть то, что выделено слишком много абстракций. Такое положение дел может свидетельствовать, что мы начали абстрагироваться даже от того, от чего абстрагироваться совсем не нужно: начали делать интерфейсы для объектов, которые просто хранят данные, или классов, чье поведение стабильно, не зависит от внешнего окружение и явно должно скрываться внутри класса, а не выставляться наружу.
Внедрение через конструктор (Constructor Injection) является базовым паттерном внедрения зависимостей и он интенсивно применяется большинством программистов, даже если они об этом не задумываются. Одной из главных целей большинства «стандартных» паттернов проектирования (GoF паттернов) является получение слабосвязанного дизайна, поэтому неудивительно, что большинство из них в том или ином виде используют внедрение зависимостей.
Так, декоратор использует внедрение зависимости через конструктор; стратегия передается через конструктор или «внедряется» нужному методу; команда может передаваться в качестве параметра, или же может принимать через конструктор окружающий контекст. Абстрактная фабрика зачастую передается через конструктор и по определению реализуется через интерфейс или абстрактный класс; паттерн Состояние принимает в качестве зависимости необходимый контекст и т.д.
Два примера, демонстрирующих применение внедрения конструктора в BCL, используют классы System.IO.StreamReader и System.IO.StreamWriter.
Оба они получают экземпляр класса System.IO.Stream в конструктор.
public StreamWriter(Stream stream);
public StreamReader(Stream stream);
Класс Stream — это абстрактный класс, выступающий в роли той абстракции, с помощью которой выполняют свои задачи StreamWriter и StreamReader. Вы можете передать любую реализацию класса Stream в их конструкторы, и они будут использовать ее. Но если вы попытаетесь передать в конструктор в качестве Stream значение null, будет генерироваться ArgumentNullExceptions.
// Декораторы
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);
Вывод
Независимо от того, используете ли вы DI контейнеры или нет, внедрение через конструктор (Constructor Injection) должен быть первым способом управления зависимостями. Его использование не только позволит сделать отношения между классами более явными, но также позволит определить проблемы с дизайном, когда количество параметров конструктора превысит определенную границу. К тому же, все современные контейнеры внедрения зависимостей поддерживают данный паттерн.
Разорвать жесткую связь между классом и его необязательными зависимостями.
Как можно разрешить внедрение зависимостей как опцию в классе, если имеется подходящее локальное умолчание?
Использованием записываемого свойства, что позволяет вызывающей стороне устанавливать его значение, если она хочет заменить поведение, применяемое по умолчанию.
Класс, использующий зависимость, должен иметь записываемое свойство с модификатором public: тип этого свойства должен соответствовать типу зависимости.
public class SomeClass
{
public ISomeInterface Dependency { get; set; }
}
Здесь SomeClass зависит от ISomeInterface. Клиенты могут передавать реализации интерфейса ISomeInterface через свойство Dependency. Обратите внимание, что в противоположность внедрению конструктора, вы не можете отметить поле свойства Dependency как «только для чтения» (Read Only), так как вызывающей стороне позволено изменять значение этого свойства в любой момент жизненного цикла класса SomeClass.
Прочие члены зависимого класса могут использовать инжектированную зависимость для выполнения своих функций, например:
public string DoSomething(string message)
{
return this.Dependency.DoStuff(message);
}
Однако такая реализация является ненадежной, поскольку свойство Dependency не гарантирует возврата экземпляра ISomeInterface. Например, код, показанный ниже, будет генерировать NullReferenceException, так как значение свойства Dependency есть null:
var sc = new SomeClass();
sc.DoSomething("Hello world!");
Данная проблема может быть устранена установкой в конструкторе экземпляра зависимости по умолчанию для свойства, скомбинированной с добавлением проверки на null в метод — установщик свойства.
public class SomeClass
{
private ISomeInterface _dependency;
public SomeClass()
{
_dependency = new DefaultSomeInterface();
}
public ISomeInterface Dependency
{
get => _dependency;
set => _dependency = value ?? throw new ArgumentNullException(nameof(value));
}
}
Трудность возникает, если клиентам будет позволено менять значение зависимости в течение жизненного цикла класса.
Что должно произойти, если клиент попытается изменить значение зависимости в течение жизненного цикла класса?
Следствием этого может быть противоречивое или неожиданное поведение класса, поэтому лучше защититься от такого поворота событий.
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), использование ее таким способом неизбежно вызовет создание неизменяемой ссылки на нее, что сведет на нет многие преимущества слабого связывания.
public class SomeClass
{
private ISomeInterface _dependency;
public void SetDependency(ISomeInterface dependency)
{
_dependency = dependency;
}
}
Если у нас есть класс, который содержит необязательную зависимость, то можно воспользоваться старым подходом с двумя конструкторами:
public class SomeClass
{
private ISomeInterface _dependency;
public SomeClass() : this(new DefaultSomeInterface())
{ }
public SomeClass(ISomeInterface dependency)
{
_dependency = dependency;
}
}
Внедрение через свойство (Property Injection) идеально подходит для необязательных зависимостей. Они вполне подойдут для стратегий с реализацией по умолчанию, но все равно, я бы рекомендовал использовать Constructor Injection и рассматривал бы другие варианты только в случае необходимости.
Автор: Дзеранов Иосиф
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/276700
Ссылки в тексте:
[1] Здесь : http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices
[2] Источник: https://habrahabr.ru/post/352530/?utm_source=habrahabr&utm_medium=rss&utm_campaign=352530
Нажмите здесь для печати.