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

    Не так давно на Хабре была опубликована статья (ссылка на топик) моего коллеги 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).

    Надеюсь на хорошую дискуссию в комментариях и жду конструктивных предложений по данному подходу от хабрасообщества. Если у вас есть замечания по орфографии и пунктуации в данной статье, то прошу писать их через личные сообщения.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 25
    • 0
      Осталось развить Criteria API и получится NHibernate )
      • +1
        Я хотел сказать, что код вида
        var account = Query.For().With(new LoginCriterion(login));

        напоминает rich-data-access синтаксис аля linq2sql, IQueryOver (NH) и т.п. Например, для IQueryOver:
        var account = QueryOver.Of().Where(x => x.Login == «userName»).GetSingleObject();
      • +7
        Без примеров извлечения реальной пользы из такого подхода, все это выглядит мягко говоря странно. Точнее, это похоже на болезненную увлеченность паттернами проектирования, когда их пытаются запихнуть всюду, куда только можно. Если мне нужно получить аккаунт по логину, то мне потребуется ровно одна строчка. У вас же, кроме инфраструктурных классов, для этой цели мне придется завести еще два класса. Я понимаю, что это просто пример, но мне кажется, он не самый подходящий. Какая ситуация может привести к тому, что будет необходимо использовать ваше решение?
        • +2
          например, когда число методов в каком-либо репозитории превысит 9000. На самом деле разделение единого интерфейса на множество объектов имеет несколько плюсов:

          1. один сложный объект разбивается на множество мелких, которые легче тестировать
          2. инкапсуляция в объекты позволяет использовать наследование для устранения дублирования (в обычном репозитории чтобы устранить дублирование в методах пришлось бы использовать делегаты/лямбды)
          • –2
            Вообще этот метод целесообразно использовать на больших проектах. Если делать проект длинной в месяц и без дальнейшей поддержки, то подойдет любой способ набить функциональность на клавиатуре.
            • 0
              Нет ли проблемы с архитектурой проекта, если количество методов в репозитории превысило 9000? К тому же, как я понял, в вашем случае вместо 9000 методов имеем (в худшем случае) 18000 классов, каждый из которых конечно описан в отдельном файле. В чем преимущество? Каким образом 18000 классов легче тестировать, чем один класс с 9000 методами?
              • 0
                Во-первых, классов не 1800, а меньше. Потому что критерии можно использовать повторно. Например, критерии для постраничной выборки можно применить к запросам разных сущностей. Во-вторых, как я уже писал, в случае классов можно эффективно использовать наследование для борьбы с дублированием. Можно строить целые иерархии запросов, помещая в базовые классы общую логику. Как такое повторить с репозиторием?
                • +1
                  1) Классов в любом случае больше чем было методов: хотя критерии и могут быть использованы повторно, но классы запросов нет, так как вы создаете по отдельному классу на каждый запрос.
                  2) В случае репозитория для борьбы с дублированием можно воспользоваться старым дедовским способом — вынести общую логику в отдельные методы!
                • 0
                  > Нет ли проблемы с архитектурой проекта, если количество методов в репозитории превысило 9000?
                  Есть. Собственно поэтому и предложена концепция разбиения репозитория на множество query объектов.

                  > В чем преимущество?
                  На самом деле в этом совершенно оторванном от реальности примере, преимущество будет в том, что команда столь огромного проекта скорее всего будет так же велика. В случае со множеством файлов будет проще мерджить изменения, внесенные командой :-)

                  > Каким образом 18000 классов легче тестировать, чем один класс с 9000 методами?
                  Хороший вопрос, кстати. На самом деле ничуть не легче, особенно если оригинальный репозиторий написан правильно и имеет минимальные зависимости.
                  • 0
                    1) Если возникла необходимость в таком большом количестве методов, то тут явно что-то не так и нужно каким-либо образом резко уменьшить их количество (например, не создавать по методу на каждый чих). Здесь же предлагается еще более увеличить сложность, породив множество мелких классов, мотивируя это некими абстрактными утверждениями. На мой взгляд класс с 100 методами (каждый из которых естественно никак не зависит от других, это же репозиторий) гораздо лучше 200 мелких классов. Для борьбы с дублированием испокон веков выделяли общую часть в отдельные методы.
                    2) Ну это спорное утверждение :) Нормальная система контроля версий без проблем смерджит изменения в обоих случаях.
                    • 0
                      1) Ну в этом абстрактром примере с 9000 методами, как я предполагаю, речь шла о публичных методах. Мне себе это, конечно, сложно представить — но по сути своей это вполне допустимо. Т.е. куча методов GetEntityOneBySomething, GetEntityTwoBySomethingElse… GetEntityNineThosandById(). Ну или не 9000 сущностей, а 900 сущностей по 10 методов.

                      Вероятно вы правы насчёт класса с сотней методов. Да, выглядит, как типичный god object. Но при этом вероятно и не нарушает ни SRP (ведь у репозитория одна обязанность — возвращать объекты из коллекции и добавлять в коллекцию), ни ISP(если только мы не предполагаем, что часть сущностей модели у нас лежат в SQL, а другая часть — в XML).

                      2) Если два программиста одновременно откроют этот файл, и один программист перенесёт существующий метод в начало файла, а второй в начале же файла напишет новый метод, то почти наверняка будет неизбежный конфликт. Пример надуманный, конечно, но я уверен, что если достаточное количество людей будет параллельно работать над одним файлом, то разрешение конфликтов для них станет каждодневным привычным делом.
                      • –1
                        Если учесть, что студия хранит список файлов в файле проекта, то эти два программиста будут ловить конфликты в любом случае. Даже чаще, чем в случае с god-репозиторием — в репозитории они смогут дописывать методы в заведомо разные места. А студия при добавлении новых файлов будет править cproj всегда в одном и том же месте.
                        • 0
                          Ваша правда, конфликты в любом случае неизбежны.
                • 0
                  3. Избавляемся от каши private/protected методов, которые вызываются в произвольных публичных методах god object'а, что позволяет лучше понимать зависимости и поведение конкретных методов за счёт лучшего разграничения контекстов.
                  • +1
                    Проблема вашего (автора) подхода заключается в:

                    1. Бессмысленном усложнении data-access слоя.
                    2. Построении абстракции над абстракцией (IQueryable)
                    3. Сложность при разработке: для того, чтобы выяснить, какие критерии поддерживаются конкретной сущностью, придется _искать_ соответствующую реализацию.
                • –2
                  <зануда>Тимур, сигнатура метода включает в себя еще тип возвращаемого значения</зануда>

                  а по теме — в комментариях к статье Саши был хороший вопрос, на который он так и не ответил: чем этот подход лучше, чем использование спецификаций?
                  • 0
                    Я на него уже отвечал

                    В случае со спецификациями приложение знает про Repository и про все спецификации.

                    В случае Query приложение знает только про интерфейс IQueryFactory
                    • 0
                      > В случае со спецификациями приложение знает про Repository и про все спецификации.
                      А какие у этого минусы? Число зависимостей (один IQueryFactory или множество репозиториев) само по себе не минус — а инъекция IQueryFactory может слегка напомнить инъекцию IocContainer'a.

                      В посте — зависимости от интерфейса IQuery<TC,TR>, который, по факту, скрывает в себе множество классов.

                      Не получается ли, кстати, что Query слегка нарушают SRP самостоятельно осуществляя и доступ к данным, и их обработку\преобразование в модель?
                      • 0
                        > Не получается ли, кстати, что Query слегка нарушают SRP самостоятельно осуществляя и доступ к данным, и их обработку\преобразование в модель?

                        Query не преобразует данные, это делает ORM. Если ORM не используется, то это делает самописный Datamapper. Query инкапсулирует запрос, не более.
                      • +1
                        > В случае Query приложение знает только про интерфейс IQueryFactory
                        И про все Query? Т.е. по сути единственное отличие IQueryFactory/Query от IRepository/ISpecification лишь в том, что IRepository помимо выборки предполагает ещё и добавление объектов в коллекцию.

                        А в случае с предложенным Тимуром методом, получается, что приложение знает о IQueryFactory и обо всех Criterion?

                        Ну разве что в этом случае спецификация разбита на параметры и, собственно, на саму спецификацию. Это, конечно, здорово, что Criterion можно использовать повторно, но зачем? При этом программисту всегда приходится помнить, для каких критерионов/сущностей в системе реализованы запросы. Или же я чего-то не понимаю?
                        • –1
                          Repository не обязательно предполагает добавления объектов в коллекцию. Просто все используют его как точку к доступа к данным, а не заморачиваются с SRP до уровня «каждому if-у — по классу».

                          Спецификация у того же Фаулера тоже разбита на критерий и какую-то абстрактную «in memory strategy» выборки. И у репозитория есть всего один метод matching(aCriteria). Так что разница IQueryFactory и IRepository — только в названии.

                          В статье — переименованная самописная реализация паттерна Repository. Естественно, работающая за счет существования в C# встроенной реализации паттерна Repository под названием IQueryProvider.
                    • 0
                      А чем вас не устроил стандартный для C# механизм IQueryable (Query), Expression (ICriterion), IQueryProvider (IQueryFactory)?
                      Пробовали ли вообще использовать IQueryable Pipes and Filters в связке с IQueryable Repository — ведь они дают то же, что ваш подход — буквально парой строк кода. Если да, то по какой причине от них отказались в пользу объемной самописной обертки?

                      Причина была достаточно веской — например, полное отсутствие юнит-тестов. Т.к. принятое решение заменить фильтры на «разрешено только те выборки, для которых написано не менее 2-х классов и 40-ка строк кода» явно создает оверхед при разработке. И, при наличии тестов, ничем не безопаснее и надежнее обычного однострочного public static IQueryable WithLogin(this IQueryable, string login) {… }.
                      • 0
                        > IQueryable Pipes and Filters
                        На мой взгляд, IQueryable хоть и делает вид, что реализует механизм Pipes and Filters, но по факту его не реализует в полной мере, выступая именно в роли Query Object, в отличии от IEnumerable. Более того, насколько я знаю, единственная технология реализующая LINQ на 100% — это Linq2Object, а следовательно рано или поздно абстракция торчащего наружу IQueryable начинает течь, что порождает различные костыли.

                        Кроме того, такой IQueryable Repository может приводить ещё и к ошибкам, связанным с тем, что этот IQueryable актуален лишь когда есть соединение с базой данных. Причем мы не можем контролировать время жизни этого объекта. Вполне может получиться, что какая-то часть системы попытается выполнить запрос уже после того, как сессия/контекст/что-либо ещё была уничтожена.

                        И это основные отличия от Query, описанного в оригинальной статье Александра. Это не Query object. Это скорее обёртка над Query Object, инкапсулирующая конкретный запрос, и по сути своей реализующая Specification.

                        Во варианте Тимура, кстати, тоже есть проблема с тем, что IQueryFor<> теоритически случайно можно отдать на сторону, потеряв над ним контроль. Хотя тут основная магия будет именно в IoC контейнере, из которого будет получен LinqProvider.
                        • 0
                          LINQ нельзя реализовать на 100% поверх SQL, т.к. такая реализация должна уметь отобразить в SQL вообще любой метод. Абстракция дырява — но Query из статьи — это еще один уровень абстракции поверх нее. В статье, например, никак не показано то самое повторное использование кода, ради которого все затевалось. Вообще все, что делает код в статье — это фильтрация уже готового IQueryable по тривиальному условию. Вы пробовали написать по аналогии что-то чуть более сложное?
                          Попробуйте написать пример с повторным использованием, у которого в случае IQueryable будут проблемы с дырявой абстракцией, а в случае с Query (поверх ILinqProvider, поверх того же IQueryable) — нет.

                          Не вытаскивайте IQueryable за определенные границы (например, вы выдавайте его за пределы BLL). И не получите проблем с соединением. В абстрактной системе в вакууме IQueryable применим в тех же границах, что и IQueryFor/IQueryFactory. Не знаю, какой магией IQueryFor можно заставить не жить слишком долго уже после доставания из контейнера, но эта же магия наверняка применима к IQueryable.

                          Ок, если коротко — для вложения столь значительных затрат в реализацию, и столь значительного увеличения сложности должна быть соизмеримая Проблема. Ок, она есть — «типа-репозитории» с тысячами методов. Если есть альтернативное решение — IQueryable Repository + Extension Methods — с очевидно меньшей сложностью, требующее в разы меньше кода, позволяющее использовать композицию, а не наследование, поддерживаемое базовым фреймворком, с гораздо более низким порогом вхождения — то новое решение должно предлагать Огромное Преимущество.
                          Если преимущества нет, или оно уровня «конфликтов станет меньше на 2%» — то новое решение — это не архитектурное решение, а добавление фабрики в ваш алгоритм.
                      • 0
                        Хотелось бы спросить у автора, продолжает ли он использовать эти методы или за 6 лет что-то поменялось?

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.