вторник, 12 февраля 2013 г.

DI Паттерны. Method Injection

Сегодня мы переходим к самому простому паттерну передачи зависимостей – передаче через аргументы метода, 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 сложно назвать очень уж распространенным паттерном в контексте управления зависимостями, тем не менее, это вполне распространенный подход в библиотеках, а также некоторых паттернах проектирования для протаскивания в операцию дополнительного контекста или стратегии, изменяемой от операции к операции.

3 комментария:

  1. Этот комментарий был удален автором.

    ОтветитьУдалить
  2. "ни один из существующих DI-контейнеров никак не сможет помочь в автоматизации получения и внедрения зависимости в метод" - но на самом то деле это можно было реализовать через System.Linq.Expressions.Expression. Что-то вроди container.Execute(Expression expression). Примерно так же, как реализован NotificationObject.RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression) в Prism. А чтобы не тоскать ссобой сам container, он мог бы резолвить Func<Expression>, которая подавалась как зависимость по аналогии с Auto-Factory делегатами. Ведь я правильно мыслю?

    ОтветитьУдалить
  3. Я не уверен, что аналогия с 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);


    ОтветитьУдалить