Постановка задачи
Предположим, у нас есть некоторый интерфейс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 должен быть всегда, т. к. он подходит под любое условие.
Комментариев нет:
Отправить комментарий