воскресенье, 16 июня 2013 г.

Выборочный Binding в Ninject

Постановка задачи

Предположим, у нас есть некоторый интерфейс
interface IMyInterface
{
}
И несколько его реализаций, разбросанных по разным сборкам
//Где-то в в какой-то сборке
class ImplementA : IMyInterface
{
}

//Где-то в в какой-то другой сборке
class ImplementB : IMyInterface
{
}
Всем сборкам доступен общий Kernel Ninject-а, через который мы будем биндить наши реализации
//В первой сборке
kernel.Bind<IMyInterface>().To<ImplementA>();
//Во второй сборке
kernel.Bind<IMyInterface>().To<ImplementB>();
Запрашиваться реализации тоже могут в разных местах кода. В том числе и разными разработчиками. Для примера, будем запрашивать их так:
var res = kernel.GetAll<IMyInterface>();
foreach( var item in res )
{
   Console.WriteLine( item.GetType().Name );
}
А теперь, собственно, наша задача - как сделать так, что бы мы получили не все реализации, а только определенные, соответствующие определенным критериям?

Именованные соединения

Самое простое, что можно сделать - это проименовать соединения и запрашивать их по имени.
//В первой сборке
kernel.Bind<IMyInterface>().To<ImplementA>().Named( "A" );
//Во второй сборке
kernel.Bind<IMyInterface>().To<ImplementB>().Named( "B" );

//Запрос реализации "A"
var res = kernel.GetAll<IMyInterface>( "A" );
foreach( var item in res )
{
   Console.WriteLine( item.GetType().Name );
}
Все это замечательно и замечательно работает. Однако, в реальной задаче реализация выбирается не "от балды" а по каким-то условиям. Для примера, предположим, что наш выбор зависит от некоторой целочисленной переменной, которую мы откуда-то получаем. Например, так:
int crit = GetCriterion();
var res = crit > 10 ? kernel.GetAll<IMyInterface>() : kernel.GetAll<IMyInterface>( "A" );
Обратите внимание, что в случае crit > 10 нам подходят обе реализации. Другими словами, я не предполагаю, что у нас всегда есть ровно одна реализация - их может быть много или не быть ни одной.
Итак, предположим, что мы построили нашу архитектуру на именованных соединениях, ее активно используют наши коллеги, запрашивая наши реализации. И все бы хорошо, но наш проект не стоит на месте - он активно развивается. И вот у нас появляется новая реализация:
class ImplementC : IMyInterface
{
}
Разработчик этой реализации утверждает, что она отлично подходит при условии crit > 100. Упс! Что бы ввести ImplementC в работу, нам придется отследить все места, зависящие от IMyInterface. А это не всегда возможно в принципе - ведь мы работаем в команде и не можем знать все, что разработали наши коллеги, используя наши библиотеки.

Метаданные

К счастью, в Ninject есть и более гибкий вариант наложить условия - это метаданные. Метаданные позволяют нам прикрепить любую информацию к нашим соединениям. К примеру, для нашей задачи будем прикреплять диапазон значений, который опишем структурой Range:
struct Range
{
   public int Min { get; set; }
   public int Max { get; set; }
}
Биндинг с метаданными будет выглядеть так:
//В первой сборке
kernel.Bind<IMyInterface>().To<ImplementA>().WithMetadata( "range", new Range { Min = int.MinValue, Max = int.MaxValue } );
//Во второй сборке
kernel.Bind<IMyInterface>().To<ImplementB>().WithMetadata( "range", new Range { Min = 10, Max = int.MaxValue } );
//Где-то еще...
kernel.Bind<IMyInterface>().To<ImplementC>().WithMetadata( "range", new Range { Min = 100, Max = int.MaxValue } );
А запрос реализаций преобразуется к следующему виду:
//Запрос реализации по условию
int crit = GetCriterion();
var res = kernel.GetAll<IMyInterface>( m =>
{
   var r = m.Get<Range>( "range" );
   return r.Min < crit && r.Max > crit;
} );
В таком варианте мы уже готовы к появлению нового интерфейса для другого диапазона чисел. Но! Что если очередной наш коллега заявит, что никакого диапазона у него нет. Зато, у него есть отличный класс:
class ImplementD : IMyInterface
{
}
И этот класс работает только с четными числами.

"Бубен" с параметрами

Понятное дело, что четные числа - это не единственное, что может прийти в голову разработчикам. А значит, что бы сделать систему по настоящему гибкой, мы должны дать возможность разработчику очередной реализации самому задать функцию-ограничитель. Как вариант можно передать эту функцию через метаданные в виде делегата. Но сейчас я хочу показать другой вариант. В Ninject предусмотрены условные соединения с помощью конструкции When.
kernel.Bind<IMyInterface>().To<ImplementA>().When( r => //условие );
При этом, нам доступны данные запроса в виде интерфейса IRequest. Остается только понять, как к этим данным привязать какую-то информацию. Например, наш критерий. Внимательно рассмотрев интерфейс IRequest, мы видим, что через него можно запросить набор параметров, переданных в запросе. Вот только, что бы получить значения этих параметров, нам нужен контекст, а его у нас нет. Вот тут-то и начинается "бубен". А что если сделать наследник класса Parameter, в котором значение будет лежать в свойстве Value и не будет зависеть от контекста:
class SimpleParameter<T> : Parameter
{
   static string DefName
   {
      get { return typeof( T ).Name; }
   }

   public SimpleParameter( T value ) : this( DefName, value ) { }

   public SimpleParameter( string name, T value )
      : base( name, value, false )
   {
      Value = value;
   }

   public T Value { get; private set; }
}
С помощью этого класса наша задача решится следующим образом:
//В первой сборке
kernel.Bind<IMyInterface>().To<ImplementA>().When( r => true );
//Во второй сборке
kernel.Bind<IMyInterface>().To<ImplementB>().When( r =>
{
   var prm = r.Parameters.OfType<SimpleParameter<int>>().SingleOrDefault();
   return prm != null && prm.Value > 10;
} );
//Четные числа
kernel.Bind<IMyInterface>().To<ImplementD>().When( r =>
{
   var prm = r.Parameters.OfType<SimpleParameter<int>>().SingleOrDefault();
   return prm != null && (prm.Value % 2 == 0);
} );

//Запрос реализации, соответствующей критерию
int crit = GetCriterion();
var res = kernel.GetAll<IMyInterface>( new SimpleParameter<int>( crit ) );
Как мы видим, все проверки перешли на сторону биндингов. А в запросе передается только критерий в виде параметра.
Обратите внимание на условие r => true - если оставить биндинг класса ImplementA без конструкции When, то мы получим этот класс только в том случае, если больше ничего не подойдет. В некоторых задачах это может быть полезно. В нашем случае ImplementA должен быть всегда, т. к. он подходит под любое условие.

Комментариев нет:

Отправить комментарий