Сегодня мы переходим к самому простому паттерну передачи зависимостей – передаче через аргументы метода, Method Injection.
Существует две разновидности паттерна под названием Method Injection. В некоторых случаях под этим паттерном понимается установка зависимостей объекта с помощью вызова метода:
public interface IDependency {} public class CustomService {
private IDependency _dependency;
public void SetDependency(IDependency dependency)
{
_dependency = dependency;
} }
По сути, этот паттерн аналогичен рассмотренному ранее паттерну Property Injection с закрытом геттером, со всеми преимуществами и недостатками. Он применяется в языках без встроенной поддержки свойств, а также может успешно применяться в языке C#, если вам это больше нравится.
Вторым типом паттерна Method Injection является передача зависимости в метод, который будет использовать ее для решения текущей задачи, а не сохраняться во внутреннем поле для последующего использования. Именно этот вид паттерна мы сегодня и рассмотрим более подробно.
Описание
Суть паттерна Method Injection заключается в передаче зависимости определенному методу, необходимой для его успешной работы.
Назначение
Предоставить классу сервиса дополнительную информацию для выполнения определенной задачи.
Применимость
Зависимости, передаваемые через конструктор или свойство являются «статическими» зависимостями и требуются объекту на протяжении всего времени его жизни, и не изменяются от одной операции к другой. Однако бывают случаи, когда зависимость (ее реальный тип или состояние) может быть разной от вызова к вызову или это единственный способ передачи зависимости, поскольку метод является статическим.
Существует несколько случаев, когда более подходящим является передача зависимости именно через метод, а не через конструктор или свойство.
1. Метод является статическим и другие варианты не подходят.
public interface ICurrencyRate
{
int GetCurrencyRate(string currency);
}
// PaymentService
public static Money CalculatePayment(ICurrencyRate currencyRate)
{
return new Money();
}
В этом же контексте используется IFormatProvider в методе double.Parse и других аналогичных методах. Иногда этот подход применим и для бизнес объектов и может использоваться, например, для статической фабрики или других подобных целей.
2. Зависимость может изменяться от операции к операции.
Существует вариант паттерна Стратегия, при котором эта стратегия не может быть передана в аргументах конструктора, поскольку она требуется лишь одному методу и может изменяться от вызова к вызову. Классическим примером такой стратегии может служить стратегия сортировки, передаваемая методу List<T>.Sort().
Этот же подход может применяться и тогда, когда некоторая стратегия доступна в месте вызова операции, а не в месте создания объекта.
// Задает "стратегию" форматирования отчета public interface IReportFormatter {
string GetFormatString(); } // ReportService public string CreateReport(IReportFormatter reportFormatter) {
return default(string); }
3. Передача локального контекста для выполнения операции.
Ряд паттернов проектирования, таких как Команда, Состояние и некоторые другие могут использовать дополнительный внешний контекст для выполнения операции. Этот же подход интенсивно используется в многопоточном программировании, когда в поток (или таймер) передается дополнительный контекст, известный вызывающему коду.
public interface ICommandContext {
int ProcessorCount { get; } } // CustomCommand public void Execute(ICommandContext context) {}
Известные применения
В составе .NET Framework этот подход используется достаточно интенсивно, как в контексте локальных стратегий, так и в контексте передачи контекста исполнения.
Локальные стратегии
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);
Команды в WPF
ICommand command = new RelayCommand(); // В реальности сюда могут приходить данные, необходимые // для выполенния команды command.Execute("value");
Дополнительный контекст в многопоточности
Все привыкли, что самый простой способ добраться до внешнего контекста заключается в захвате внешних переменных. Тем не менее, есть и олдскульный подход, который заключается в явном протаскивании этого контекста через метод:
// Использование контекста для передачи данных в другой поток. var context = new CustomViewModel(); var thread = new Thread(o => {
var localContext = (CustomViewModel) o; }); thread.Start(context);
Ограничения
Главная проблема этого паттерна заключается в том, что ни один из существующих DI-контейнеров никак не сможет помочь в автоматизации получения и внедрения зависимости в метод. Поскольку вызов метода происходит явно, то вызывающему коду придется самостоятельно решить, откуда взять ему экземпляр зависимости – создать экземпляр конкретного класса или переложить эту ответственность на свой верхний уровень.
Кроме того, к этому паттерну нужно отнестись с осторожностью, если он применяется потому, что некоторая зависимость нужна лишь одному методу в классе и нам лень из-за этого протаскивать ее через конструктор. Иногда этот подход оправдан, но он также может означать низкую внутреннюю связность (low cohesion) класса и нарушение Принципа Единой Обязанности: точно ли все нормально, что некоторая зависимость нужна лишь одной операции и не нужна другим методам? Может здесь скрыто два класса?
В отличие от Constructor Injection и Property Injection данный паттерн носит более «локальный» характер и не является типовым паттерном управления зависимостями в приложении. Тем не менее, если под зависимостями понимать «локальный» контекст или локальную стратегию, то этот паттерн вполне применим на практике.
Заключение
Method Injection сложно назвать очень уж распространенным паттерном в контексте управления зависимостями, тем не менее, это вполне распространенный подход в библиотеках, а также некоторых паттернах проектирования для протаскивания в операцию дополнительного контекста или стратегии, изменяемой от операции к операции.
Я не уверен, что аналогия с RaisePropertyChanged здесь подходит.
ОтветитьУдалитьНо вот аналогия с частичным применением функций (из функционального программирования) подходит точно.
// ReportService public static string CreateReport(IReportFormatter reportFormatter, double data)
{}
Func createReportWithFormatter =
ReportService.CreateReport;
Func createReport =
d => createReportWithFormatter(container.Resolve(), d);
// Теперь можем использовать createReport так:
string report = createReport(42);