Pull to refresh

Получение экземпляра класса запроса по сигнатуре его интерфейса

.NET *
Не так давно на Хабре была опубликована статья (ссылка на топик) моего коллеги AlexanderByndyu, описывающая уход от использования Repository в сторону применения связки QueryFactory + классы запросов Query. При этом в комментариях разгорелся весьма интересный диспут, касающийся целесообразности приведенного в статье решения. Было достаточно много интересных отзывов, среди которых особенно выделялись высказывания о том, что, дескать, QueryFactory не нужен и является лишней обузой, мешающей безболезненному добавлению, изменению и удалению классов запросов. В данной статье я хочу показать подход, который позволяет избавиться от применения QueryFactory, через активное использование IoC контейнера. Данную организацию работы со структурой классов запросов мы использовали в одном из наших недавних проектов, где в качестве IoC использовался Castle.Windsor.


Описание подхода


Для начала я опишу основную идею, заложенную в описываемом подходе. Суть его близка к тому, как компилятор определяет, какая версия перегруженного метода используется в конкретном случае, а именно идентификация по сигнатуре. И если в том случае, когда речь идет о методе его сигнатура – это порядок, число и типы передаваемых аргументов, то в случае если необходимо получить конкретную реализацию интерфейса из IoC контейнера, то сигнатура определяется набором generic-параметров в интерфейсе. Возможно некоторое расхождение в понятиях с теми, кто привык под сигнатурой интерфейса понимать набор его методов, но в рамках данной статьи предлагаю принять вышеназванное толкование понятие.
Надеюсь в целом понятно, а если нет, то при просмотре приведенного примера все встанет на свои места.

Реализация


Положим у нас есть общий для всех запросов интерфейс IQuery<,>:
public interface IQuery<in TCriterion, out TResult>
  where TCriterion : ICriterion
{
  TResult Execute(TCriterion criterion);
}


* This source code was highlighted with Source Code Highlighter.

Соответственно его сигнатура определяется конкретной реализацией ICriterion, т.е. объектом, содержащим данные, необходимые для построения запроса (в основном для предикатов фильтрования Where), а также типом возвращаемого результата. Таким образом при наличии единственной реализации интерфейса IQuery<,> с определенными типами generic-параметров TCriterion и TResult, зная эти типы можно получить реализацию интерфейса.
Ниже приведен код в классе WindsorInstaller, который регистрирует все реализации интерфейса IQuery<,> в IoC контейнере.
public class WindsorInstaller : IWindsorInstaller
{
   public void Install(IWindsorContainer container, IConfigurationStore store)
   {
     var queries = AllTypes.FromAssemblyNamed("Domain.NHibernate")
       .BasedOn(typeof (IQuery<,>))
       .WithService.FirstInterface()
       .Configure(x => x.LifeStyle.Transient);

       container.Register(queries);
   }
}


* This source code was highlighted with Source Code Highlighter.

В данном примере происходит получение всех реализаций интерфейса IQuery<,> из сборки Domain.NHibernate их содержащей и регистрация полученных типов в контейнере как реализаций их первого интерфейса (который опять же и есть IQuery<,>).
Для дальнейшего использования конкретного запроса в контроллере или, положим, в обработчике формы, необходимо написание небольшого, но очень важного вспомогательного класса. Но для начала хотелось бы привести пример его использования, чтобы можно было понять, предоставляемые им возможности и внешний вид (для разработчика) механизма получения реализации запроса по сигнатуре интерфейса. Назовем этот пример листинг 1.
var account = Query.For<Account>().With(new LoginCriterion(login));

* This source code was highlighted with Source Code Highlighter.

В данном примере мы получаем запрос по сигнатуре его интерфейса, от которого мы хотим, чтобы он возвращал сущность Account, а поиск производил по логину этого самого аккаунта. Логин передается через LoginCriterion. Соотвественно, чтобы вышеуказанный код был прозрачнее привожу код запроса, который будет использован в вышеуказанном примере, а также код класса LoginCriterion.
public class FindAccountByLoginQuery : LinqQueryBase<Account>, IQuery<LoginCriterion, Account>
{
    public FindAccountByLoginQuery(ILinqProvider linqProvider)
        : base(linqProvider)
    {
    }

    public Account Execute(LoginCriterion criterion)
    {
        return Query()
            .SingleOrDefault(x => x.Login.ToLower() == criterion.Login.ToLower());
    }
}

public class LoginCriterion : ICriterion
{
   public LoginCriterion(string login)
   {
     Login = login;
   }

   public string Login { get; set; }
}


* This source code was highlighted with Source Code Highlighter.

Теперь, что касается вспомогательного кода, то он представлен двумя интерфейсами:
public interface IQueryBuilder
{
   IQueryFor<TResult> For<TResult>();
}

public interface IQueryFor<out T>
{
   T With<TCriterion>(TCriterion criterion) where TCriterion : ICriterion;
  
   T ById(int id);
  
   IEnumerable<T> All();
}


* This source code was highlighted with Source Code Highlighter.

… и их реализациями:
public class QueryBuilder : IQueryBuilder
{
   private readonly IDependencyResolver dependencyResolver;

   public QueryBuilder(IDependencyResolver dependencyResolver)
   {
     this.dependencyResolver = dependencyResolver;
   }

   public IQueryFor<TResult> For<TResult>()
   {
     return new QueryFor<TResult>(dependencyResolver);
   }

   #region Nested type: QueryFor

   private class QueryFor<TResult> : IQueryFor<TResult>
   {
     private readonly IDependencyResolver dependencyResolver;

     public QueryFor(IDependencyResolver dependencyResolver)
     {
        this.dependencyResolver = dependencyResolver;
     }

     public TResult With<TCriterion>(TCriterion criterion) where TCriterion : ICriterion
     {
        return dependencyResolver.GetService<IQuery<TCriterion, TResult>>().Execute(criterion);
     }
    
     public TResult ById(int id)
     {
        return dependencyResolver.GetService<IFindByIdQuery<TResult>>().Execute(new IdCriterion(id));
     }

     public IEnumerable<TResult> All()
     {
        return dependencyResolver.GetService<IFindAllQuery<TResult>>().Execute(new EmptyCriterion());
     }    
   }

   #endregion
}


* This source code was highlighted with Source Code Highlighter.

По сути, для реализации нашего подхода можно было бы ограничиться единственным интерфейсом, но тогда пришлось бы явно указывать generic-параметр типа реализации ICriterion, что отяжелило бы интерфейс IQueryBuilder. При указанной же реализации явно указывается лишь тип значения, возвращаемого из запроса. Непосредственное получение экземпляра запроса из IoC контейнера и его выполнение осуществляет класс QueryFor<>. При этом используется интерфейс к IoC контейнеру, предоставленный внутренними средствами ASP.NET MVC3, IDependencyResolver. В нашем случае в итоге все запросы к контейнеру будут делегированы Castle.Windsor.
Одной из главных особенностей использования QueryBuilder является регистрация его в IoC контейнере как реализации интерфейса IQueryBuilder. В связи с инжекцией зависимостей в конструктор, а также введенным в ASP.NET MVC3 механизмом подстановки реализации для всех публичных свойств объектов, типы которых зарегистрированы в контейнере, если сам экземпляр объекта также получен из контейнера, становится возможным следующее (абстрактный пример, код открытия UnitOfWork опущен).
public class AccountController : Controller
{
   public IQueryBuilder Query { get; set; }

   public ActionResult Index(string login)
   {  
     var account = Query.For<Account>().With(new LoginCriterion(login));

     // делаем тут что-нибудь полезное
   }
}


* This source code was highlighted with Source Code Highlighter.


Заключение


Означенный подход позволяет не только избавиться от QueryFactory, что делает безболезненным добавление, изменение и удаление классов запросов, но и позволяет полностью абстрагироваться от понятия запроса в контексте места его использования. Я бы даже назвал подобный механизм получения запросов «настоящим», т.к. он оперирует двумя ключевыми понятиями тесно связанными с запросом в разрезе подхода CQS: это входные критерии выборки и возвращаемый результат. Казалось бы различные реализации ICriterion могут «захламить» код, но на самом деле это не так, т.к. их можно использовать повторно для различных запросов.
Подобный подход получения реализации по сигнатуре интерфейса можно использовать не только для запросов, но и для реализаций других generic-интерфейсов, особенно в том случае, если этих реализаций множество и/или их состав подвержен частым изменениям. Например, подобными объектами могут служить команды (из подхода CQS).

Надеюсь на хорошую дискуссию в комментариях и жду конструктивных предложений по данному подходу от хабрасообщества. Если у вас есть замечания по орфографии и пунктуации в данной статье, то прошу писать их через личные сообщения.
Tags:
Hubs:
Total votes 23: ↑18 and ↓5 +13
Views 12K
Comments 25
Comments Comments 25