В прошлый раз разобрали базовые, самые часто используемые паттерны внедрения зависимостях. Сегодня разберем, остальные два, которые также используются при проектировании гибких систем. Сегодня поговорим, про внедрение через метод и про окружающий контекст. Поехали!
Как можно внедрить зависимости в класс, если они различны для каждой операции?
Путем передачи в качестве параметра метода. Если при каждом вызове метода используется другая зависимость, вы можете передать ее через параметр метода.
Вызывающая сторона передает зависимость как параметр метода при каждом его вызове. Эта процедура не сложнее, чем сигнатура представленного ниже метода:
Часто зависимость будет представлять некоторый вид контекста для операции, передаваемого как соответствующее значение:
Если сервис использует зависимость, в нем прежде всего должна выполняться проверка на null.
Внедрение метода лучше всего использовать, когда при каждом вызове методу задается другая зависимость. Это может происходить в случае, когда зависимость сама по себе представляет некоторое значение.
Существует несколько случаев, когда более подходящим является передача зависимости именно через метод, а не через конструктор или свойство:
Внедрение через метод (Method Injection) сложно назвать очень уж распространенным паттерном в контексте управления зависимостями, тем не менее, это вполне распространенный подход в библиотеках, а также некоторых паттернах проектирования для протаскивания в операцию дополнительного контекста или стратегии, изменяемой от операции к операции.
Как можем мы сделать зависимость доступной в каждом модуле без включения в каждый API компонента сквозных аспектов приложения?
Окружающий контекст доступен любому потребителю через статическое свойство или метод. Потребляющий класс может использовать его следующим образом:
Чтобы быть полезным в сценариях внедрения зависимостей, контекст сам по себе должен являться абстракцией. При этом должна иметься возможность его модификации извне — это означает, что свойство Current должно разрешать запись значений (быть writable).
Окружающкс контекст по структуре похож на антипаттерн Сервис Локатор. Различие между ними состоит в том, что окружающий контекст предоставляет только экземпляр единственной, сильно типизированной зависимости, тогда как Сервис Локатор предоставляет экземпляры для любой запрошенной вами зависимости.
Неявность. При работе с окружающим контекстом вы не сможете, просто взглянув на интерфейс, с уверенностью сказать, используется ли данный контекст конкретным классом.
Сложность реализации. Корректной реализации окружающего контекста может быть достаточно. Как минимум, вы должны гарантировать, что контекст всегда находится в пригодном для использования состоянии — то есть при запросах к нему не должно возникать никаких исключительных ситуаций типа NullReferenceExceptions только из-за того, что одна реализация контекста была удалена без замены ее на другую.
Проблемы с выполнением в ASP.NET.Если окружающий контекст использует TLS, могут возникнуть проблемы при запуске приложения в ASP.NET, поскольку появляется вероятность изменения потоков в определенные моменты жизненного цикла интернет-страниц. При этом не гарантировано, что сохраненные в TLS данные будут скопированы из старого потока в новый. В такой ситуации для хранения специфических данных запроса нужно использовать текущий HttpContext, а не TLS.
Если вам нужно запросить зависимость сквозного аспекта на получение ответа, не включенного в оригинальный интерфейс, вы можете применять окружающий контекст, при условии, что для него имеется локальное умолчание. Так вы сможете объединять собственно контекст с поведением, заданным по умолчанию, которое используется всеми клиентами без явного конфигурирования.
Данная статья окончанием паттернов внедрения зависимостей. Мы рассмотрели 4 паттерна внедрения зависимостей, такие как внедрение через конструктор, внедрение через свойство, внедрение через метод и окружающий контекст. Чтобы решить, какой шаблон выбрать для своей конкретной задачи, ниже представлен алгоритм выбора соответствующего паттерна предложенный Марком Симаном. Пользуйтесь как можно чаще. А если данный алгоритм не поможет, выбирайте внедрение конструктора — вы в этом случае никогда не совершите ужасных ошибок.
Внедрение через метод
Как можно внедрить зависимости в класс, если они различны для каждой операции?
Путем передачи в качестве параметра метода. Если при каждом вызове метода используется другая зависимость, вы можете передать ее через параметр метода.
Как это работает
Вызывающая сторона передает зависимость как параметр метода при каждом его вызове. Эта процедура не сложнее, чем сигнатура представленного ниже метода:
public void DoStuff(ISomeInterface dependency)
Часто зависимость будет представлять некоторый вид контекста для операции, передаваемого как соответствующее значение:
public string DoStuff(SomeValue value, ISomeContext context)
Если сервис использует зависимость, в нем прежде всего должна выполняться проверка на null.
Когда следует использовать внедрение метода
Внедрение метода лучше всего использовать, когда при каждом вызове методу задается другая зависимость. Это может происходить в случае, когда зависимость сама по себе представляет некоторое значение.
Существует несколько случаев, когда более подходящим является передача зависимости именно через метод, а не через конструктор или свойство:
- Метод является статическим и другие варианты не подходят. В этом же контексте используется IFormatProvider в методе double.Parse и других аналогичных методах.
- Зависимость может изменяться от операции к операции. Существует вариант паттерна Стратегия, при котором эта стратегия не может быть передана в аргументах конструктора, поскольку она требуется лишь одному методу и может изменяться от вызова к вызову. Классическим примером такой стратегии может служить стратегия сортировки, передаваемая методу List<T>.Sort(). Этот же подход может применяться и тогда, когда некоторая стратегия доступна в месте вызова операции, а не в месте создания объекта.
- Передача локального контекста для выполнения операции. Ряд паттернов проектирования, таких как Команда, Состояние и некоторые другие могут использовать дополнительный внешний контекст для выполнения операции. Этот же подход интенсивно используется в многопоточном программировании, когда в поток (или таймер) передается дополнительный контекст, известный вызывающему коду.
Примеры использования
public interface ICommandContext
{
int ProcessorCount { get; }
}
// CustomCommand
public void Execute(ICommandContext context)
{}
Локальные стратегии
IFormatProvider provider = new NumberFormatInfo { NumberDecimalSeparator = ";" };
// Задаем "стратегию" разбора double
var value = double.Parse("1;1", provider);
IComparer<int> comparer = Comparer<int>.Default;
var list = new List<int> {3, 4, 1};
// Передаем "стратегию" сортировки
list.Sort(comparer);
var task = Task.Run(() => { });
TaskScheduler taskScheduler = TaskScheduler.Current;
// Задаем "стратегию" запуска "продолжения" задачи
task.ContinueWith(t => { },
taskScheduler);
Заключение
Внедрение через метод (Method Injection) сложно назвать очень уж распространенным паттерном в контексте управления зависимостями, тем не менее, это вполне распространенный подход в библиотеках, а также некоторых паттернах проектирования для протаскивания в операцию дополнительного контекста или стратегии, изменяемой от операции к операции.
Окружающий контекст
Как можем мы сделать зависимость доступной в каждом модуле без включения в каждый API компонента сквозных аспектов приложения?
Окружающий контекст доступен любому потребителю через статическое свойство или метод. Потребляющий класс может использовать его следующим образом:
public string GetMessage()
{
return SomeContext.Current.SomeValue;
}
Чтобы быть полезным в сценариях внедрения зависимостей, контекст сам по себе должен являться абстракцией. При этом должна иметься возможность его модификации извне — это означает, что свойство Current должно разрешать запись значений (быть writable).
public abstract class SomeContext
{
public static SomeContext Current
{
get
{
if (Thread.GetData(Thread.GetNamedDataSlot("SomeContext")) is SomeContext ctx)
return ctx;
ctx = Default;
Thread.SetData(Thread.GetNamedDataSlot("SomeContext"), ctx);
return ctx;
}
set => Thread.SetData(Thread.GetNamedDataSlot("SomeContext"), value);
}
private static SomeContext Default = new DefaultContext();
public abstract string SomeValue { get; }
}
Окружающкс контекст по структуре похож на антипаттерн Сервис Локатор. Различие между ними состоит в том, что окружающий контекст предоставляет только экземпляр единственной, сильно типизированной зависимости, тогда как Сервис Локатор предоставляет экземпляры для любой запрошенной вами зависимости.
Когда применять окружающий контекст?
- Нужна возможность делать запросы к контексту. Если вам требуется только запрашивать данные (например, о текущем времени)
- Определен подходящее локальное умолчание. Он будет использоваться неявно, поэтому важно, чтобы контекст функционировал корректно.
- Необходима гарантированная доступность. Даже когда определено подходящее локальное умолчание, необходимо обеспечить его защиту от установки в значение null.
Важные характеристики
Достоинства | Недостатки |
Не загрязняет API | Неявность |
Всегда доступен | Трудно добиться корректной реализации |
- | Может неправильно работать в некоторых средах исполнения |
Неявность. При работе с окружающим контекстом вы не сможете, просто взглянув на интерфейс, с уверенностью сказать, используется ли данный контекст конкретным классом.
Сложность реализации. Корректной реализации окружающего контекста может быть достаточно. Как минимум, вы должны гарантировать, что контекст всегда находится в пригодном для использования состоянии — то есть при запросах к нему не должно возникать никаких исключительных ситуаций типа NullReferenceExceptions только из-за того, что одна реализация контекста была удалена без замены ее на другую.
Проблемы с выполнением в ASP.NET.Если окружающий контекст использует TLS, могут возникнуть проблемы при запуске приложения в ASP.NET, поскольку появляется вероятность изменения потоков в определенные моменты жизненного цикла интернет-страниц. При этом не гарантировано, что сохраненные в TLS данные будут скопированы из старого потока в новый. В такой ситуации для хранения специфических данных запроса нужно использовать текущий HttpContext, а не TLS.
Известные способы применения
- Безопасность реализуется через интерфейс System.Security.Principal.IPrincipal, ассоциированный с каждым потоком. Вы можете получить (get) или установить (set) текущего владельца потока, используя методы доступа из Thread.CurrentPrincipal.
- Thread.CurrentCulture и Thread.CurrentUICulture позволяют получить доступ и модифицировать культурный контекст текущей операции. Многие предназначенные для форматирования API, такие как синтаксические анализаторы и конверторы типов, неявно используют текущую культуру, если никакая другая явно не установлена.
- Класс Trace не связан с конкретным потоком, а совместно используется по всему приложению. Задействуя метод Trace.Write, вы можете записать трассировочное сообщение из любого места
Заключение
Если вам нужно запросить зависимость сквозного аспекта на получение ответа, не включенного в оригинальный интерфейс, вы можете применять окружающий контекст, при условии, что для него имеется локальное умолчание. Так вы сможете объединять собственно контекст с поведением, заданным по умолчанию, которое используется всеми клиентами без явного конфигурирования.
Данная статья окончанием паттернов внедрения зависимостей. Мы рассмотрели 4 паттерна внедрения зависимостей, такие как внедрение через конструктор, внедрение через свойство, внедрение через метод и окружающий контекст. Чтобы решить, какой шаблон выбрать для своей конкретной задачи, ниже представлен алгоритм выбора соответствующего паттерна предложенный Марком Симаном. Пользуйтесь как можно чаще. А если данный алгоритм не поможет, выбирайте внедрение конструктора — вы в этом случае никогда не совершите ужасных ошибок.