1 августа 2011 г.

Проблемный шаблон Repository

Как уже обсуждалось в Domain-Driven Design: Repository, реализация шаблона Repository фактически превращается в статический класс (даже, если у него нет слова static) с большим количеством методов. Кроме этого возникает рад вопросов к реализации шаблона Repository, на которые нет простых ответов.

Какой репозиторий использовать, если метод выборки работает с несколькими сущностями?

Если нам надо выбрать Account по Project, то мы пойдем за этим методом в AccountRepository или ProjectRepository? А если выборка идет по 3-м сущностям, то где искать нужный метод?

Что делать, если разные репозитории должны использовать закрытые методы друг друга?

Можно из одного репозитория вызывать другой, но это превратит код в паутину. Можно один репозиторий унаследовать от другого, но это приведет к огромным God-object'ам.

Может использовать вообще один репозиторий на весь проект?

Это еще хуже, потому что это будет нетестируемый god-object с безграничной ответственностью.

Может репозиторий - это просто построитель запросов?

Тогда логика построения запроса и его выполнения будет разбросана по коду. Например, можно создать часть запроса в сервисе, добавить в него условий в контроллере, а выполнить запрос в модели. Логика в запросах неизбежно начнет дублироваться. К тому же такие «размазанные» запросы будет сложно тестировать.

Надо ли дублировать методы в сервисе и репозитории?

Это вопрос о том, стоит ли давать контроллерам доступ напрямую к репозиториям или только через сервисы. Если нет, то тогда вызовы репозитория приходится оборачивать в сервисах.

Вопросы можно продолжать, но главное то, что шаблон Repository утратил свою актуальность. Либо репозитории разрастаются, либо логика запросов расплывается по всем частям проекта.

Делим Repository на Query

Самым простым и рабочим решением является разделение репозиториев на небольшие объект-запросы. В проекте эта реализация тесно переплетается с концепции CQRS. Мы применяем ее уже около года, поэтому решение хорошо обкатано и проверено на рабочих проектах.

Суть в том, что у нас есть набор объектов, каждый из который делает одну операцию чтения. Только чтения, не записи. Одна логическая операция чтения равна одному объекту запроса.

Например, нам надо найти Account по адресу электронной почты. Создаем класс FindAccountByEmailQuery.

public class FindAccountByEmailQuery
{
    private readonly ILinqProvider linqProvider;
    private string email;

    public FindAccountByEmailQuery(ILinqProvider linqProvider, string email)
    {
        this.linqProvider = linqProvider;
        this.email = email;
    }

    public Account Execute()
    {
        return linq.Query()
            .Where(x => x.Email == email)
            .SingleOrDefault();
    }
}

Объект делает ровно столько, сколько написано в его названии, тестировать очень просто, логики очень мало. Может многократно использоваться в проекте.

Инфраструктура

Со временем у нас будет довольно много маленьких объектов типа *Query, поэтому будет удобно сделать доступ к ним через фабрику.

public interface IQueryFactory
{
    [CanBeNull]
    Account FindAccountByEmail(string email);

    // ...
}

public class QueryFactory : IQueryFactory
{
    private readonly ILinqProvider linqProvider;

    public QueryFactory(ILinqProvider linqProvider)
    {
        this.linqProvider = linqProvider;
    }

    public Account FindAccountByEmail(string email)
    {
        return new FindAccountByEmailQuery(linqProvider, email).Execute();
    }
}

Теперь интерфейс IQueryFactory можно инжектировать в другие объекты. Например, выберем данные в методе AccountController:

public class AccountController : Controller
{
   private readonly IQueryFactory queryFactory;
   private readonly IUnitOfWorkFactory unitOfWorkFactory;

   public AccountController(IQueryFactory queryFactory, IUnitOfWorkFactory unitOfWorkFactory)
   {
       this.queryFactory = queryFactory;
       this.unitOfWorkFactory = unitOfWorkFactory;
   }

   public ActionResult LogOn(string email)
   {
       using(unitOfWorkFactory.Create())
       {
           Account account = queryFactory.FindAccountByEmail(email);

           // ...

           return View();
       }
   }
}

Структура проекта, где используются Query, может выглядеть так:

Осталось реализовать инжекцию зависимостей, интерфейсы ILinqProvider и IUnitOfWorkFactory. Пример для реализации доступа к данным через NHibernate можно посмотреть на Google Code — nhibernate2-unitofwork.

Бестелесный IQueryFactory

Интерфейс IQueryFactory получается довольно громоздким, поэтому его лучше заменить на интерфейс с одним методом. См. статью Заменяем QueryFactory на бестелесный IQueryFactory.

Заключение

Если ваши репозитории уже являются «всемогущими» объектам, которые невозможно тестировать, то самое время для рефакторинга. Если ваши репозитории еще только начали свой жизненный путь, то на ранней стадии вы сможете решить будущие проблемы. Главное, что теперь понятно, куда двигаться.

Посмотрите ссылки, приведенные ниже, чтобы еще глубже понять проблему работы с шаблоном Repository и концепцию решения.


Ссылки

DDDD: Master-Detail Question

Repository is the new Singleton

DDD : Command Query Separation as an Architectural Concept

Brownfield CQRS

Query Objects vs Methods on a Repository

DDD: Specification or Query Object

Wither the Repository

Repositories don’t have save methods

141 комментарий:

  1. Выглядит достаточно перекомпилкейчено. У нас IQueryFactory просто возвращает IQueriable, екстеншен методы уже делают то что у вас делают Query.

    ОтветитьУдалить
  2. @Mike Chaliy

    Может появится соблазн наращивать IQueriable, где не надо.

    ОтветитьУдалить
  3. С разделением запросов на объекты - ок, но тебе не кажется, что QueryFactory у тебя со временем и будет тем самым Repository с кучей методов?
    Можно, конечно, добавить в фабрику метод Create(..), но и здесь потенциально возможны проблемы.

    ОтветитьУдалить
  4. >Какой репозиторий использовать, если метод >выборки работает с несколькими сущностями?

    зависит от того, в контексте какого агрегата идет работа.

    ОтветитьУдалить
  5. Пощупать код на эту тему можно тут https://github.com/xelibrion/Lab/tree/master/QueryObject

    ОтветитьУдалить
  6. @xelibrion

    На счет агрегата соглашусь.

    Спасибо за ссылку на код.

    ОтветитьУдалить
  7. >>Можно из одного репозитория вызывать другой, но это превратит код в паутину. Можно один репозиторий унаследовать от другого, но это приведет к огромным God-object'ам.

    А можно выделить отдельную зависимость, как и в случае с любой дублирующейся логикой

    >>Тогда логика построения запроса и его выполнения будет разбросана по коду.

    Я с момента обсуждения IQueryable/IEnumerable репозиториев продал душу дьяволу и стал использовать IQueryable-репозитории. Правда, следую несколькоим правилам. Вся логика инкапсулируется: в частности условия накладываются только через спецификации, а за пределами репозитория я позволяю себе наращивать IQueryable только View-specific методами: сортировка, выборка и т. д. Пока меня устраивает.

    Query выглядит интересной альтернативой Repository, но при этом какой-то чересчур многобуквенной, что ли. Да и к тому же, по сути, это обертка над одним методом классического репозитория с вытекающим отсутствием гибкости.

    ОтветитьУдалить
  8. Паттерн репозитория предназначен для решения следующей проблемы - у вас в проекте большая и сложная бизнсе-модель, запросы к бизнес-сущностям разбросаны по всему проекту, при этом появляется куча дублированного кода.

    Мы используем репозитории для следующих задач:
    1. решение проблем с дублированием кода
    2. организация кода
    3. юнит-тестирование

    Вот мой взгляд на использование репозитриев:

    >> реализация шаблона Repository фактически превращается в статический класс
    т.к. репозиторий не содержит состояние, то можно использовать и статический класс, но, по мне так, это есть не есть хорошо, особенно для юнит-тестирования

    >>Какой репозиторий использовать, если метод выборки работает с несколькими сущностями? Если нам надо выбрать Account по Project, то мы пойдем за этим методом в AccountRepository или ProjectRepository?
    конечно AccountRepository, а токак то не звучит - из хранилища (repository) проектов мы вытягиваем акаунты

    >>Что делать, если разные репозитории должны использовать закрытые методы друг друга?
    уникать этой ситуации, честно говоря я не могу придумать реальный пример, когда это действительно надо

    >>Может использовать вообще один репозиторий на весь проект?
    лучше отказаться от репозиториев и использовать QueryBuilder паттерн, т.к. скорей всего в вашем проекте не более 20 бизнес-сущностей

    >>Может репозиторий - это просто построитель запросов?
    из названия понятно, что это хранилище бизнес-сущностей, реализация которого инкапсулирует запросы к мапперу

    >>Надо ли дублировать методы в сервисе и репозитории?
    т.к. репозиторий относится к слою предметной области, то почему бы и нет?

    ОтветитьУдалить
  9. >> В QueryFactory нет логики.

    Но при этом ты будешь вынужден изменять ее всякий раз, когда добавляется новый специфичный запрос.

    ОтветитьУдалить
  10. >>Но при этом ты будешь вынужден изменять ее всякий раз, когда добавляется новый специфичный запрос.

    Да и к тому же это, по сути, ServiceLocator. И чтобы понять, что нужно застабить, нужно лезть в код (из конструктора не понятно)

    ОтветитьУдалить
  11. С запросом суть понятна. А как предлагаешь выполнять команды (запись)?

    ОтветитьУдалить
  12. Писать целый класс ради строки

    .Where(x => x.Email == email)
    .SingleOrDefault();

    Очень смешно, если честно :-)

    ОтветитьУдалить
  13. @Nikolay Sergeev, а что с этим не так?

    ОтветитьУдалить
  14. >> @Idsa Я с момента обсуждения IQueryable/IEnumerable репозиториев продал душу дьяволу и стал использовать IQueryable-репозитории.
    Хехе, 20% твоей души - мои. Еще немного - и ты осознаешь, что на самом деле давно используешь нормальный несамописный QueryObject, но будет уже слишком поздно...

    >> new FindAccountByEmailQuery(linqProvider, email).Execute();
    а как же D из soliD?
    Меня немного смущает название QueryFactory. Это не реализация Factory (из GoF), и строит она не запросы.
    1. Начало статьи - много текста про плохой репозиторий - класс с public-интерфейсом из кучи методов FindSomeObjectByXXX(...).
    2. Много букв.
    3. Конец статьи - много текста про новый, хороший класс QueryFactory, с public-интерфейсом из кучи методов FindSomeObjectByXXX(...).

    В чем суть? Замена репозитория на репозиторий другой реализации решила хоть одну из изначальных проблем? Тем более что проблема одна - создание "репозитория запросов" вместо нормального репозитория. Репозиторий должен предоставлять "collection-like interface for accessing domain objects", а не быть помойкой запросов.

    Но в целом интересно. Продолжайте в том же духе :)

    PS комментарии в IE не добавляются.

    ОтветитьУдалить
  15. @Idsa

    > А можно выделить отдельную зависимость, как и в случае с любой дублирующейся логикой

    Например?

    > Query выглядит интересной альтернативой Repository

    А вы попробуйте хотя бы на тестовом проекте, иначе преимущества не так очевидны.

    ОтветитьУдалить
  16. @agat

    > т.к. репозиторий не содержит состояние, то можно использовать и статический класс, но, по мне так, это есть не есть хорошо, особенно для юнит-тестирования

    Да, это еще вредит DIP и затрудняет инжектирование репозитория в другие объекты.

    > уникать этой ситуации, честно говоря я не могу придумать реальный пример, когда это действительно надо

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

    > лучше отказаться от репозиториев и использовать QueryBuilder паттерн, т.к. скорей всего в вашем проекте не более 20 бизнес-сущностей

    В нашем проекте гораздо больше 20.

    > из названия понятно, что это хранилище бизнес-сущностей, реализация которого инкапсулирует запросы к мапперу

    Посмотрите комментарии к статье http://blog.byndyu.ru/2011/01/domain-driven-design-repository.html и удивитесь сколько есть мнений на этот счет :)

    > т.к. репозиторий относится к слою предметной области, то почему бы и нет?

    Потому что сервит превращается во второй репозиторий. Если в репозитории метод называется GetActiveAccounts, то в сервисе появится FindActiveAccounts с одной строчкой вызовом метода репозитория. Это становится излишним, когда повтояется по 40-50 методов.

    ОтветитьУдалить
  17. @Sergey Zwezdin

    Ну да, это обычная фабрика. Можно разруливать все зависимости автоматически через байдинг в IoC контейнере. Для этого надо использовать конвенции вместо конфигурации. Тоже вариант.

    ОтветитьУдалить
  18. @Idsa

    > Да и к тому же это, по сути, ServiceLocator. И чтобы понять, что нужно застабить, нужно лезть в код (из конструктора не понятно)

    Можно пример кода? Лучше через http://pastebin.com

    ОтветитьУдалить
  19. @Алексей Романовский

    Команды, которые C-QRS, в MVC мы делаем через FormHandler'ы. Хотел дать ссылку на какую-нибудь статью про эту тему, но почему-то не нашел :)

    Либо еще поищу и дам ссылку, либо напишу про это.

    ОтветитьУдалить
  20. @Pasha

    Спасибо за вопрос, видимо я не полностью описал преимущества. Кстати, вы почитали ссылки в конце статьи?

    Самое главное, что мы уходим от не тестируемых репозиториев к маленьким тестируемым классам с единственной отвественностью и понятной логикой. Фактически облегчаем себе разработку.

    Для начала я бы посоветовал вам создать тестовый проект и поразрабатывать его в этом стиле хотя бы несколько вечеров. Тогда вы увидите как легка эта концепция в понимании и расширении системы. Это особенно важно для работы в команде.

    > а как же D из soliD?

    По-моему нарушения нет. Реализация фабрики скрыта за интерфейсом. Сама фабрика знает про всех, кого произоводит. Но в ней никакой логики, поэтому ничего страшного. Опять же можно использовать конвенции и сделать в фабрике единственный метод, который будет работать с IoC-контейнером.

    ОтветитьУдалить
  21. >>Хехе, 20% твоей души - мои

    Апостол Павел? :)

    >>Еще немного - и ты осознаешь, что на самом деле давно используешь нормальный несамописный QueryObject, но будет уже слишком поздно...

    Ну хорошо, пусть будет QueryObject. И что? А почему поздно? До конца света время еще есть

    ОтветитьУдалить
  22. @Nikolay Sergeev
    Писать целый комментарий ради одной строки - очень смешно, если честно

    ОтветитьУдалить
  23. @Александр Бындю, @Алексей Романовский

    Вот статья от Jimmy Bogard
    http://lostechies.com/jimmybogard/2011/06/22/cleaning-up-posts-in-asp-net-mvc/

    У нас примерно так.

    ОтветитьУдалить
  24. @hazzik

    Спасибо за ссылку. Еще была статья с названием типа "Посадите ваши контроллеры на диету". Кто найдет, дайте пожалуйста ссылочку.

    ОтветитьУдалить
  25. Это не статья, это презентация Богарда на MVCConf. В самой презентации мало слайдов. Код и презентация вот тут: http://headspringlabs.codeplex.com/SourceControl/changeset/view/936eb41fbdb4

    ОтветитьУдалить
  26. @Александр Бындю, http://blog.alagad.com/2007/04/18/put-your-controllers-on-a-diet-use-a-service-layer/

    ОтветитьУдалить
  27. @hazzik @Idsa

    Спасибо, очень полезные ссылки!

    ОтветитьУдалить
  28. Выглядит, как будто QueryFactory превратился в один большой репозиторий. Саня, если не заставлять меня тратить несколько вечеров на тестовые проекты, в чем выгода?

    ОтветитьУдалить
  29. @Мурадов Мурад

    В том, что у тебя было 10 огромных репозиториев с 1000 срок кода в каждом, а стало 50 маленьких объектов, про которые знает только QueryFactory.

    Создание нового запрос сводится к созданию маленького класса через TDD. Создать класс *Query очень просто, потому что в нем мало логики. Сравни это с добавлением еще одного метода в огромный репозиторий.

    При изменении запроса, мы заходим внутрь одного маленького класса и меняем его, это в отличие от изменения запроса внутри репозитория.

    В самой QueryFactory нет никакой логики, в отличие от больших репозиториев. QueryFactory даже тестировать не надо.

    ОтветитьУдалить
  30. Не соглашусь про огромные репозитории. Все зависит от сущностей и их зависимостей, но по крайней мере у меня репозитории огромными не становятся.
    Если будет новый запрос, то в случае с репозиторием будет добавлен только один метод, а для QueryFactory - класс запроса и метод в фабрике.
    Тестирование репозитория и множества классов запросов по объему все равно будут равны.
    Приятно, конечно, что запрос будет в отдельном классе, но как минимум выгода не очевидна.

    ОтветитьУдалить
  31. Более того, если использовать Castle Windsor, то у QueryFactory даже реализации нет, только интерфейс

    ОтветитьУдалить
  32. @xelibrion

    Да, так будет еще проще. В статье я показал суть решения и один из способов реализации.

    ОтветитьУдалить
  33. @Мурадов Мурад

    Это до тех пор, пока у тебя маленький проект и в репозиториях по 5-6 методов. Если проект год делает 10 человек, то репозитории становятся ооочень большими :) Представь себе, что у тебя 15-20 агрегатов, т.е. примерно 15-20 репозиториев с кучей методов.

    Я могу судить об этой проблеме еще и по письмам, которые мне приходят. Основной вопрос, который задают: "Как справится со сложностью репозиториев/сервисов?"

    ОтветитьУдалить
  34. >> Я могу судить об этой проблеме еще и по
    >> письмам, которые мне приходят. Основной
    >> вопрос, который задают: "Как справится со
    >> сложностью репозиториев/сервисов?"

    Обычно репозитарии становятся большими, когда репозитрии пользуют как слой доступа к данным или рут агрегаты постороенны неправильно. Или когда пытаются через репозитарии читать данные для отображения. Я вполне допускаю что репозитарий может вырасти до больше чем 5-6 методов, но 1) эти методы обычно две три строки 2) это всеже скорее исключение, и не стоит одно исключение решать усложнением всего проекта.

    ОтветитьУдалить
  35. @Александр
    >> Спасибо за вопрос, видимо я не полностью описал преимущества.
    Нет, преимущества как раз расписаны хорошо. Проблема в другом - все поднятые тобой начале статьи проблемы связаны с внешним интерфейсом репозитория, с временем его жизни, и со структурой зависимостей остальных классов от репозитория.

    А предложенный QueryFactory - это реализация того же самого внешнего интерфейса IRepository через делегирование. Да, хорошая реализация. В определенных обстоятельствах - лучше обычной пачки методов. Решает ли она проблемы внутренней реализации? Возможно. Решает ли она проблемы, поднятые в начале статьи - нет.

    Задумайтесь, как отразится переход с IRepository на IQueryFactory на остальном проекте (исключая разрезание класса-репозитория на мелкие классы-запросы). Повсюду в коде будет заменен IRepository на IQueryFactory и .. все? Проблема наличия god object решена переименованием интерфейса?

    Ты предлагаешь посидеть пару вечеров и попробовать. Ок, взял старый sandbox проект, переписал внутреннюю реализацию репозитория с обертки над DataContext на агрегирование пачки мелких классов-запросов - ок, имеет смысл.
    Переименовал везде в проекте IRepository на IQueryFactory, но просветления не почувствовал.

    Александр, потрать и ты 15 минут. Возьми текущий проект, переименуй IQueryFactory обратно в IRepository, удали внутреннюю реализацию фабрики/репозитория, вместе с классами-запросами. И спроси другого разработчика, какой паттерн используется - старый плохой репозиторий, или новый хороший QueryFactory. А потом удиви его.

    ОтветитьУдалить
  36. @Александр Бындю, @hazzik
    >Команды, которые C-QRS, в MVC мы делаем через FormHandler'ы. Хотел дать ссылку на какую-нибудь статью про эту тему, но почему-то не нашел :)

    >Самым простым и рабочим решением является разделение репозиториев на небольшие объект-запросы.

    Спасибо за ссылки. Я знаком с CQRS в MVC.
    Тогда так задам вопрос: обычно в хэндлерах для записи данных используется репозиторий. Если у нас теперь есть только объекты-запросы, то кто будет заниматься сохранением данных?
    Подразумевается, что репозиторий разделяется также и на объекты-"команды"?

    ОтветитьУдалить
  37. @Mike Chaliy
    Допустим у нас 5 агрегатов на весь проект. Тогда на каждый агрегат может приходится по 20-30 разных запросов. Это как раз и есть размер репозитория.

    А переход на Query это совсем не усложнение проекта, вы попробуйте.

    ОтветитьУдалить
  38. @Pasha

    > Повсюду в коде будет заменен IRepository на IQueryFactory

    Я бы так сказал. Все I*Repository (IProductRepository, IAccountRepository и т.д.) будут заменены на IQueryFactory.

    > Проблема наличия god object решена переименованием интерфейса?

    Проблема решена разделением ответственности на маленькие объекты. Это понятно?

    ОтветитьУдалить
  39. @Алексей Романовский

    Я же попросил прочитать ссылки после статьи. Вы это видимо не сделали и решили сразу позадавать вопросы :)

    Ну ладно, вот эта ссылка http://richarddingwall.name/2009/10/22/repositories-dont-have-save-methods

    Сохранением объектов занимается UnitOfWork.

    ОтветитьУдалить
  40. @Александр Бындю

    В http://blog.byndyu.ru/2010/07/2-unit-of-work_10.html ты рассказывал про UnitOfWork и показывал там примеры интерфейса IUnitOfWork с Commit и Rollback. Правильно ли я сейчас понял, что IUnitOfWork нужно расширить и реализовать еще методы, например, Save(object) и Delete(object), с помощью которых и будут сохраняться и удаляться объекты?
    Если все так, то все встает на свои места!

    ОтветитьУдалить
  41. @Алексей Романовский

    Точно! :) Вот такой UoW http://martinfowler.com/eaaCatalog/unitOfWork.html

    Только у нас нет метода Delete, он плохо влияет на производительность БД. При удалении строки СУБД приходится перестраивать индексы, каскадно удалять сущности если необходимо. Проще делать объекты IsDeleted. Но это уже по выбору.

    ОтветитьУдалить
  42. @Александр Бындю

    Спасибо! У меня прямо-таки пазл сложился! Я в восторге! Я как-то однобоко смотрел на UoW.

    Теперь надобность в репозиториях отпадает напрочь. Действительно очень простое, красивое и аккуратное решение получилось.

    ОтветитьУдалить
  43. @Алексей Романовский

    Вы попробуйте в проекте, если возникнут вопросы/проблемы, то будем вместе решать. Можно здесь http://groups.google.ru/group/dotnetconf

    ОтветитьУдалить
  44. @Александр:

    Почему бы не возвращать интерфейс IAccountQuery с опеределенными возможностями и различными служебными возможностями(заранее реализованными):
    uoq.Accounts()./*спец.*/WhereEmail("test@account.ru").OrderByCreationDate()./*служеб.*/Take(100).Skip(20).ExcludeTypes().Query() //IE of Account
    uoq.Accounts().WhereEmail("test@account.ru").OrderByCreationDate().Take(100).Skip(20).ExcludeTypes().Uniquely.Query() // Account, exception if count > 1
    uoq.Accounts().WhereEmail("test@account.ru").OrderByCreationDate().Take(100).Skip(20).ExcludeTypes().KeyInRangeOf(1,2,3,45,6,7,8,8,9).Any.Query() // bool
    uoq.Accounts().WhereEmail("test@account.ru").OrderByCreationDate().Take(100).Skip(20).Count.Query() //long
    uoq.Account().WhereEmail("test@account.ru").ExcludeTypes().Query() //Maybe of Account

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

    При этом может существовать спец. запрос, семантика которого, закладывается в название: IGoldAccountQuery

    uoq./*без extension метода.*/QueryOver().Using().KeyInRangeOf(1,2,3,45,6,7,8,8,9).Any.Query() // bool
    uoq.QueryOver().Using().Query() // //IE of Account


    Так же если речь идет о букве Q в CQRS, то почему у нас используется IUnitOfWorkFactory, мы же не собираемся в данном контексте делать работу,
    разделение между С и Q должно быть явным, как на уровне семантики названий методов, так и на уровне реализации методов(даже в случае использования одной модели запись-чтение).
    Т.е. использовать, как мне кажется, надо IUnitOfQueryFactory, которая создает IUnitOfQuery:

    using(uoq = unitOfQueryFactory.Create())
    // Возможно не имеет возможность задать IsolationLevel или вообще не создает транзакцию,
    //а использует неявные, в отличии от IUnitOfWork
    {
    Account account = queryFactory.FindAccountByEmail(email);

    // ...
    //uoq.Complete(); IUnitOfQuery не имеет такого метода в отличии от IUnitOfWork
    return View();
    }

    Так использование такого объекта, по-мимо выделения явности разделения таких важных понятий как команды и запросы,
    позволяет добиваться оптимизаций запросов при использовании их через IUnitOfQuery, например использование IStatelessSession для NH, что существенно увеличивает
    производительность запросов (для этого нужно всего лишь создать ISessionDecorator, который инжектится в
    запросы и объединяет набор фичей из IStatelessSession и из ISession). Так же в UnitOfWork запросы можно строить только над IAggregateRoot(хотя тут я пока еще сомневаюсь,
    возможно над IQueryableEntity), а вот через IUnitOfQuery запросы можно строить над чем угодно(над любой IEntity), т.к. запросам не нужно заботится о границах консистентности,
    блокировках и прочих вещах.

    Если посмотреть на код, который я привожу в самом начале, то можно понять, что ваш пример я бы переписал таким образом:
    using(uoq = unitOfQueryFactory.Create())
    {
    /*UniqueAccount() это ex. m.: QueryOver().Using()*/
    Account account = uoq.UniqueAccount().WhereEmail("test@account.ru").Query();
    }

    В данном случае IUnitOfQuery - это сервис локатор для запросов(и для репозиториев, кстати тоже).

    Пойдем далее, и избавимся от фабрики блоков-единиц(запросов и работы):

    using(uoq = new UnitOfQuery())
    {
    Account account = uoq.UniqueAccount().WhereEmail("test@account.ru").Query();
    }

    Чувствую, как кто-то уже сказал: "фиии, это же запрещенный оператор new...." :)
    Да, я не вижу все от чего у меня зависит контроллер, но зато его коструктор содержит только те зависмости, которые действительно важно видеть в запросном/операционном слое
    и данный код по прежнему остается таким же тестируемым(конечно, при грамотной реализации UnitOfWharever).

    ОтветитьУдалить
  45. Просто, как я понял, в статье решается проблема декомпозиции отдельных репозиториев с одновременной композицией всех репозиториев в один класс. Это наверное не то, что мы все ждали в рассказе про то, как вы делаете CQRS ))

    ОтветитьУдалить
  46. >> Проблема решена разделением ответственности на маленькие объекты. Это понятно?
    Да. Понятно. Решена проблема внутренней организации репозитория.
    Проблемы, поднятые в начале статьи - не решены [если в результате свести все в QueryFactory]. Это понятно?

    Я честно попытался последовать вашему совету. Применил подход из статьи к своему текущему проекту. Судя по комментариям - я вообще единственный, кто это сделал. Честно описал полученный резульат в комменте на хабре, стараясь не устраивать полотно в ответе. Получил 3 минуса в карму :) Отличная благодарность :)

    ОтветитьУдалить
  47. @Nikita Govorov

    Спасибо за подробный комментарий.

    Я лично за то, чтобы запросы строились на LINQ, мне кажется это избавляет от лишней работы, но если вам проще было сделать свой язык запросов, то тоже вариант.

    Самое главное, что вы строите запрос прямо в контроллере и этот метод фактически не инкапсулирует логику запроса где-либо. Я могу построить запрос в котроллере или в сервисе. При этом часть логики запросов обязательно начнет дублироваться.

    А вы использовали описанный вами подход?

    > Просто, как я понял, в статье решается проблема декомпозиции отдельных репозиториев с одновременной композицией всех репозиториев в один класс.

    Нет, QueryFactory не делает запросы в отличие от Repository, а только создает объекты, т.е. являет обычной фабрикой объектов. Сам код репозиториев теперь разбит на маленькие *Query.

    ОтветитьУдалить
  48. > если в результате свести все в QueryFactory

    Еще раз. QueryFactory не имеет никакой логики. Посмотрите внимательно. QueryFactory не строит запросы, не делает запросы, не сохраняет и не выбирает данные - это фабрика объектов и только один из возможных вариантов работы с объектами типа *Query.

    > Получил 3 минуса в карму :)

    Ну это же Хабр. Я вот тоже в карму получил минус и ни одного плюса :)Даже не хочу спрашивать о причине.

    ОтветитьУдалить
  49. >> Сам код репозиториев теперь разбит на маленькие *Query.
    http://martinfowler.com/eaaCatalog/repository.html

    сам код репозиториев должен быть разбит на маленькие inMemoryStrategy и при реализации классического паттерна. Чем ваш подход отличается?

    Еще раз. Классический репозиторий не имеет никакой логики. Посмотрите внимательно. Репозиторий не строит запросы, не делает запросы, не сохраняет и не выбирает данные - это фабрика объектов и только один из возможных вариантов работы с объектами типа *inMemoryStrategy. Хм.

    ОтветитьУдалить
  50. @Pasha

    > сам код репозиториев должен быть разбит на маленькие inMemoryStrategy

    Это откуда вы взяли?

    А вот из определения:
    > where query construction code is concentrated...adding this layer helps minimize duplicate query logic

    Ну дак есть там работа с запросами или нет?

    И самое главное - Repository не нужен.

    Сохранять и удалять объекты нужно в UnitOfWork, выбирать данные с помощью *Query, изменять данные с помощью команд.

    ОтветитьУдалить
  51. >> Это откуда вы взяли?
    а откуда вы взяли что не должен?
    Посмотрите на диаграмму в PoEAA, по ссылке. там же черным по белому нарисовано, в каком месте критерий (не запрос) превращается в коллекцию объектов people who satisfied the criteria. это не в коде репозитория происходит.

    >> Ну дак есть там работа с запросами или нет?
    нет, там есть код типа new SomeQuery(). не надо понимать текст буквально как "прямо в теле репозитория должен быть код запросов".

    ОтветитьУдалить
  52. > Посмотрите на диаграмму в PoEAA, по ссылке. там же черным по белому нарисовано, в каком месте критерий (не запрос) превращается в коллекцию объектов

    Ну это можно записать так:

    class AccountRepository
    {
    public IList GetAccount()
    {
    return linqProvider.Query().Where(x => x.Id == 10);
    }
    }

    Вот вам и inMemoryStrategy на LINQ. Можно в него и спецификации передавать и еще много всего можно. Я просто не понял, почему вы так критично собрались разбивать его на стратегии.

    > прямо в теле репозитория должен быть код запросов

    Вот вам код на LINQ внутри репозитория.

    И самое главное - Repository не нужен.

    ОтветитьУдалить
  53. >> Ну это можно записать так
    а можно так:
    class AccountRepository
    {
    public IList GetAccount()
    {
    public Account GetAccount(int id)
    {
    return new FindAccountByIdQuery(linqProvider, 10).Execute();
    }
    }
    }

    он же не перестанет быть репозиторием, который не нужен.
    можно даже так:
    class QueryFactory
    {
    public IList GetAccount()
    {
    public Account GetAccount(int id)
    {
    return new FindAccountByIdQuery(linqProvider, 10).Execute();
    }
    }
    }
    о, кажется я только что переименованием класса решил все проблемы.

    кому какая разница как репозиторий устроен внутри. проблемы, обозначенные в начале статьи, не имею отношения к внутренней реализации.

    >> Я просто не понял, почему вы так критично собрались разбивать его на стратегии.
    потому что вы начали разбивать его на стратегии. только назваете их *Query.

    да, такой репозиторий - не нужен. вот только он такой же и внутри и снаружи, как ваша фабрика.

    ОтветитьУдалить
  54. @Pasha

    Эта статья для тех, кто столкнулся с проблемами при использовании шаблона Repository, которые связаны с разрастанием классов и ответственностей. Предлагается убрать Repository и разделить его на объекты запросов. Как вариант можно использовать QueryFactory. Ключевое здесь: "как вариант" = не обязательно. Мой коллега в ближайшее время собирается выложить пост, где нет QueryFactory.

    Если у вас есть свой подход, то я могу только порадоваться.

    ОтветитьУдалить
  55. @Александр:

    >Я лично за то, чтобы запросы строились на LINQ, мне кажется это...
    Это уже зависит от реализации запроса(супер-типа для этого запроса). У меня есть запросы и с HQL и даже с SQL внутри, linq и QueryOver(Criteria) - это вообще обычное дело.
    >Самое главное, что вы строите запрос прямо...
    Не всегда:
    >При этом может существовать спец. запрос, семантика которого, закладывается в названии: IGoldAccountQuery
    Я возвращаю QueryObject в общими доп. возможностями необх. для буквы Q, а не сразу результат запроса как Вы.
    Это дает возможность дать операционному слою те возможности, в которых он действительно нуждается.


    >А вы использовали описанный вами подход?
    Да конечно.

    >Нет, QueryFactory не делает запросы в отличие...

    Вы разбиваете методы репозитория на отдельные классы(*Query) и объединяете все репозитории(IProductRepository, ICustomerRepository etc) в один объект(пусть даже он не содержит логики) QueryFactory.
    Я это имел ввиду.


    Если смотреть на запрос не с точки зрения внутреннего устройства, а с точки зрения потребителя, то он должен предоставлять возможность наращивания,
    иначе легче вызвать метод репозитория. Вы же поменяли внутренние понятия, но для потребителя вместо гранулированных по агрегатам (фун. понятиям и прочему) интерфейсов
    репозиториев, предоставляете все те же методы сваленные в один объект(удобно инжектить, но не удобно искать). Посмотрите на ваш подход со стороны потребителя
    (разработчика опер. слоя или слоя домена), в чем выгода? Меньше инъекций? Так, как я писал выше, их вообще можно убрать. Может быть стоит поискать ответы на ваши вопросы
    к проблемному репозиторию в другом? Вы произвели декомпозицию огромных репозиториев, ну и все, пусть IProductRepository, ICustomerRepository etc остаются, в них тоже не будет
    логики, но для потребителя здесь не появляется QueryObject - это просто организация кода в пределах слоя доступа к данным основанного на репозиториях,
    и при этом не теряется его грануляция.

    ОтветитьУдалить
  56. @Александр. Статья хорошая. Меня смущает что во второй части вы предлагаете репозиторий вернуть, но под другим названием.

    Как вариант - хорошее слово. Но вы же этот вариант настойчиво рекомендуете.

    Обязательно прочитаю следующую статью.

    ОтветитьУдалить
  57. @Nikita Govorov

    Согласен с тем, что вы написали.

    > Вы произвели декомпозицию огромных репозиториев, ну и все, пусть IProductRepository, ICustomerRepository etc остаются

    Да, как вариант.

    ОтветитьУдалить
  58. @Pasha

    Как я уже писал, главное - разбить большие репозитории на небольшие объекты.

    Если QueryFactory будет возвращать IQuery (как сделано у нас в проекте), то можно будет наращивать запросы, что удовлетворит @Nikita Govorov, т.к. QueryFactory будет фабрикой запросов :)

    ОтветитьУдалить
  59. >> Если QueryFactory будет возвращать IQuery (как сделано у нас в проекте)

    Это большое если. у нас репозиторий возвращает IQueryable. Т.е. репозиторием он называется только потому, что выполняет ту же задачу что репозиторий у Фаулера. Настощий репозиторий - IQueryProvider, но все разработчики принимают эту абстракцию как должное.

    ОтветитьУдалить
  60. @Pasha

    Я лично против того, чтобы репозиторий возвращал IQueryable. Подробно обсуждалось в статье и комментария к посту Domain-Driven Design: Repository

    ОтветитьУдалить
  61. Забавно, что пришли к IQueryable. Ведь в IQueryable-репозиториях проблем с разрастанием не наблюдается (спасибо спецификациям), а значит изначально смысла в создании Query нет (по крайней мере если следовать принципу "главное - разбить большие репозитории на небольшие объекты"

    ОтветитьУдалить
  62. Процитирую из статьи:

    "Тогда логика построения запроса и его выполнения будет разбросана по коду. Например, можно создать часть запроса в сервисе, добавить в него условий в контроллере, а выполнить запрос в модели. Логика в запросах неизбежно начнет дублироваться. К тому же такие «размазанные» запросы будет сложно тестировать."

    ОтветитьУдалить
  63. Александр, а ты не против десятка перегрузок в зависимости от разных OrderBy, Select, Include и т. д.?

    ОтветитьУдалить
  64. @Idsa

    Перегрузок чего? Можно хотя бы небольшой пример.

    ОтветитьУдалить
  65. Насчет разбрасывания по коду. Это можно контролировать. Я писал выше, что за пределами репозитория, который накладывает переданную спецификацию, я позволяю себе накладывать только View-specific методы, которые не несут в себе логики. А значит вся логика остается инкапсулированной в спецификации.

    ОтветитьУдалить
  66. Вот реальный пример классического репозитория: http://pastebin.com/guwfbrgv Говоря о перегрузках, я имею в виду жесть вроде GetTagsWithLocalizations vs. GetTagsWithParentsAndLocalizations

    ОтветитьУдалить
  67. @Idsa

    Я понял, это перегрузки для подкачки связанных коллекций, так?

    Если честно, для оптимизации скорости иногда приходится так делать.

    Может вы знаете другое решение?

    ОтветитьУдалить
  68. >> Я лично против того, чтобы репозиторий возвращал IQueryable
    а IQuery можно? судя по названию - этот свой IQueryable. Чем он лучше стандартного?

    ОтветитьУдалить
  69. @Александр,

    Подкачка значений - это лишь частный случай. Еще могут быть нужны разные сортировки. Или, например, иногда нужно выбрать всю сущность, а иногда лишь одно свойство (тот же айди). Иногда нужно выполнить Single, иногда FirstOrDefault и т. д. И на все это по-хорошему нужно делать свои перегрузки.

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

    И вот я как представлю, что все эти перегрузки нужно растащить по отдельным классам, сразу энтузиазм по отношению к Query пропадает (Павел привел хороший пример на Хабре, умножив 20 репозиториев на 30 методов и получив 600 Query - впечатляет). Хотя теоретически паттерн интересный, и разного рода сквозная логика на него ложится лучше. Хотя в случае репозиториев похожего эффекта можно добиться дополнительным слоем абстракции в виде сервисов.

    ОтветитьУдалить
  70. Привет, Александр, коллеги!

    Спасибо за статью!
    Вопрос: как все же в приведенном подходе избавитсяь от дублирования кода между классами запросов?
    Скажем один из классов-запросов выбирает Accounts по критерию "Пол + год рождения", а в другом случае нужна выборка по критерию "Пол + год рождения + какой-то статус"?

    ОтветитьУдалить
  71. @ALive

    Мы делаем это выделением базового класса. В этом случае наследование оправдано. В случает, когда у репозитория 30 методов, наследование не оправдано.

    ОтветитьУдалить
  72. @Александр Бындю
    насчет логики построения запроса и разбрасывания. репозиторий инкапсулирует логику построения запроса к данным в базе. то, что у вас называется Query - это спецификации, запросы на массивами объектов. Репозиторий никак не решает проблему разброса логики построения спецификаций. Он принимает готовые спецификации как параметры, и только умеет их преобразовывать в запросы к хранилищу данных внутри себя.

    Как решать проблему разброса логики создания спецификаций? да как угодно, группировать их в SpecificationFactory (QueryFactory) в вашем варианте. Строить спецификации в методах расширения, и группировать в классы по сущностям (pipes and filters).

    Как обрабатывать подгрузку коллекций? например добавить механизм interception, и позволить управлять загрузкой явно, из бизнес-логики, через методы расширения.

    Но к изначальным проблемам, поднятым в топике, это не имеет никакого отношения.

    ОтветитьУдалить
  73. @Pasha

    > Он принимает готовые спецификации как параметры, и только умеет их преобразовывать в запросы к хранилищу данных внутри себя

    Зачем он тогда нужен? Если можно сделать запрос на Linq и ORM его выполнит. Хватит класса Query.

    ОтветитьУдалить
  74. >> Зачем он тогда нужен? Если можно сделать запрос на Linq и ORM его выполнит. Хватит класса Query.

    Самописный "репозиторий" - только для адаптации ORM к контексту проекта.

    то, что вы обычно называют ORM - это пакет готовых реализаций паттернов Repository/Data Mapper/UoW/Lazy Load/Identity Map. Зачем задавать вопрос "зачем он нужен" если вы каждый день пользуетесь репозиторием, встроенным в ваш ORM?

    ОтветитьУдалить
  75. @Pasha

    Это я к тому, что можно передавать ILinqProvider прямо в Query минуя Repository.

    ОтветитьУдалить
  76. @Pasha
    >судя по названию - этот свой IQueryable. Чем он лучше стандартного?
    Он инкапсулирует технологию доступа к данным за контролируемые границы. Как используя IQueryable запросить по данным, которых нет в доменной модели, но есть в модели отображения данных на сущности, или эти данные приватны в доменной модели?
    Должна быть граница, выставление IQueryable наружу усложняет проведение(контроль) границы, но при этом облегчает разработку, не спорю, особенно если его потребителем является тот же разработчик, который разрабатывал схему маппинга.

    ОтветитьУдалить
  77. @Nikita Govorov, а так ли нужно инкапсулировать технологию доступа к данным? Не в идеальном мире, а в реальности.

    ОтветитьУдалить
  78. @Idsa

    В команде больше одного человека, однозначно да.

    ОтветитьУдалить
  79. @Nikita Govorov
    Естественно, IQueryable нельзя выставлять наверх из BL/доменной модели. Это интерфейс для выборки данных, между ORM и BL.
    Наверх, в представление, уходят строго списки. Представление ничего не знает о репозитории (или обертке).

    Не совсем представляю себе ситуацию "нет данных в доменной модели" - т.е. она не соотвествует предметной области? Можно пример?

    Данные приватны в доменной модели - ок, не вижу проблем. Код построения expression/спецификаций тоже находится строго в BL, и вполне видит приватные данные.

    @Александр
    >> Это я к тому, что можно передавать ILinqProvider прямо в Query минуя Repository.
    Если учесть, что ILinqProvider - это реализация репозитория в ORM, а репозиторий - это просто обертка, то ваше предложение читается как "можно передавать репозиторий прямо в Query минуя обертку". Да, можно.

    @Idsa layer skipping нехорошая вещь без острой необходимости. но технология доступа данным и так обычно инкапсулирована в ORM. Попробуй выполнить чистый SQL в L2S, имея только IQueryable :)

    ОтветитьУдалить
  80. @Idsa
    Как используя IQueryable запросить по данным, которых нет в доменной модели, но есть в модели отображения данных на сущности, или эти данные приватны в доменной модели?

    ОтветитьУдалить
  81. @Pasha
    >Естественно, IQueryable нельзя...
    Как я понимаю вы выставляете IQueryable в операционный слой? Что такое BL?

    >Не совсем представляю...
    http://ayende.com/blog/4054/nhibernate-query-only-properties
    >Данные приватны в доменной модели - ок...
    Приведите, пожалуйста, пример, я перестал понимать вашу точку зрения.

    ОтветитьУдалить
  82. @Nikitа Business logic layer. Не уверен насчет точного русского перевода.

    >> http://ayende.com/blog/4054/nhibernate-query-only-properties
    Экстенш-метод HasRecentPosts для IEnumerable, с кодом вроде:
    return blogs.Where(b => b.posts.IsRecent().Any());

    ну и обычный IsRecent: return posts.Where(p => p.PostDate > ....)

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

    >> Приведите, пожалуйста, пример, я перестал понимать вашу точку зрения.
    Тогда лучше вы уточняйте, что за приватные данные. Не имеющие маппинга в базу?

    ОтветитьУдалить
  83. @Pasha
    Про приватные данные в доменной модели, первый пример приходящий в голову, есть User у него есть скрытое(private) поле пароль, как через IQueryable написать запрос производящий поиск по имени пользователя и паролю?

    ОтветитьУдалить
  84. @Nikita поле приватное, в базе не хранится, и маппинга для него нет? тогда в общем случае - никак - пользователя нельзя найти в базе по паролю.

    В частном - тот же L2S позволяет создать фейковое public свойство Password, замапить его на колонку Password в базе, и в маппинге указать storage-ем скрытое поле. Фильтрация и сортировка по паролю работать будет при выборке через LINQ.
    Но непосредственное обращение к свойству с целью прочитать или записать будет бросать исключение/делать хэширование при записи.

    Другие провайдеры тоже такие финты позволяют, скорее всего.

    ОтветитьУдалить
  85. Как я уже говорил, лично я считаю данный подход лишь очередной разновидностью реализации паттерна Specification, который в данном случае еще и инкапсулирует сам запрос, помимо предиката.

    Чем это лучше GenericRepository + Specification, в общем-то не ясно. Те же проблемы )

    Касательно CQRS так часто тут упоминаемого, то не уверен что оно именно CQRS, а не CQS. Именно это видно из примеров.

    ОтветитьУдалить
  86. > Самое главное, что мы уходим от не тестируемых репозиториев к маленьким тестируемым классам с единственной отвественностью и понятной логикой.

    имхо в данном случае вы производите лишь DAL, который, к тому же, судя из комментов, требует IoC и прочей ерунды, что усложняет саму сущность.

    Repository же в основе своей очень прост.
    Скорее всего проблемы с ним возникают из-за недопонимания его предназначения.

    Ведь корни его лежат в Java, где никаких IQueriable не было. А теперь люди пытаются мешать совершенно разные парадигмы.

    ОтветитьУдалить
  87. >>А теперь люди пытаются мешать совершенно разные парадигмы.

    Вы так говорите, как будто это плохо :)

    ОтветитьУдалить
  88. @Pasha
    Поле приватное, в базе хранится, и маппинг для него есть.

    ОтветитьУдалить
  89. > Вы так говорите, как будто это плохо :)

    Это плохо когда отсутствует понимание того, для каких целей это создавалось.

    Начнём с того, что изначально самое плохое - это попытка применить Repository в обычном веб-приложении. Где кроме данных и DTO в принципе ничего не нужно)
    Отсюда и выплывает куча топиков и проблем :-)

    ОтветитьУдалить
  90. Подводя итоги сегодняшнего вечера, можно сделать вывод, что, вместо одного GenericRepository и 20 Specification мы имеем 25 QueryObject'ов.

    Осталось понять чем это помогает)

    Понимаю чем удобно, а так же опасно, протаскивать IQueriable. Но в данном случае мы боремся с ветряными мельницами.

    ОтветитьУдалить
  91. @Nikita
    >> Поле приватное, в базе хранится, и маппинг для него есть.

    раз ваш провайдер поддерживает маппинг приватных полей (сомнительная фича, если честно), то он должен поддерживать указание приватных полей как storage для паблик свойств. Т.е. все как я выше расписал - фейковое свойство, и проблема решена.

    Хотя сама идея изменять приватные поля извне класса, да еще и неявно - это дичайшее нарушение инкапсуляции, после которого вообще нельзя доверять модификаторам доступа во всем проекте. По сравнению с ним - протаскивание IQueryable куда угодно - детские шалости :)

    ОтветитьУдалить
  92. @Pasha
    ORM должен уметь отображать на любые поля(свойства). В модели мое поле явно приватное(ну или protected), и я не хочу давать к нему доступ из вне. Соответственно я не могу использовать IQueryable для запроса по таким данным. Заведение фейковых полей и прочие костыли в модели не интересно в принципе.

    Материализация объектов ORM'м, так же как допустим и десериализация, это низкоуровневые процессы, задача которых, восстановить внутреннее состояние объекта.
    Вы так не считаете? У вас все свойства сущностей имеют public prop {get;set}?

    ОтветитьУдалить
  93. Вы можете использовать IQueryable для запроса по таким данным, без костылей. Внутри класса User вы сможете отфильтровать IQueryable по паролю.

    Извне, теоретически - нет, потому что вне класса User о существовании поля ничего не известно. Нельзя фильтровать по том, чего нет.

    На практике, если очень хочется - то можно кодом собрать вообще любой Expression>, и передать его в Where. Снаружи будет тот же фильтр .WithPassword(pass). Внутри - будет код, в котором будет упоминание свойства по имени, типа "password". По концентрации анти-ООД - это то же, что задание фильтра как "select users from user where password = ?".

    Этот блог вроде как посвящен ООП/ООД, и за упоминание свойства по имени я рискую получить пожизненный бан.

    Да, у нас все поля сущностей { get; set; }. Десериализация, IMO, внешний для сущности процесс. Кто-то не доверяет своим разработчикам, кто-то - бинарными сериализаторам.

    ОтветитьУдалить
  94. @Pasha
    >Вы можете использовать IQueryable для запроса по таким данным, без костылей.
    >Внутри класса User вы сможете отфильтровать IQueryable по паролю.
    Действительно, это вариант. Вот только странно со стороны будет смотреться, да и ваши любимые ext. meth. нельзя во вложеннных классах объявлять.
    >Извне, теоретически - нет, потому что вне класса User о существовании поля ничего не известно. Нельзя фильтровать по том, чего нет.
    >По концентрации анти-ООД ...
    >Этот блог вроде как посвящен ООП/ООД, и за упоминани...
    О существовании поля известно в слое доступа к данным. И этим нужно там пользоваться, magic strings, HQL, SQL все равно. Смешение парадигм неизбежно, не стоит этому противиться,
    лишь бы была изоляция.
    >Кто-то не доверяет своим разработчикам...
    Вопрос не в доверии к разработчикам, вопрос в возникновении нередко необходимости инкапсуляции внутреннего состояния объектами модели,
    поэтому IQueryable по своей природе не в состоянии помочь в таких случаях.

    ОтветитьУдалить
  95. @Nikita ну ок, пусть будет смешение парадигм, не ООП, ext. meth с упоминанием приватного поля по имени. зато изоляция. It Depends.

    ОтветитьУдалить
  96. @Gengzu

    > имхо в данном случае вы производите лишь DAL, который, к тому же, судя из комментов, требует IoC и прочей ерунды, что усложняет саму сущность.

    Зависит от реализации, обязательное использование IoC не нужно. Ну и конечно Query ничего не знает про IoC, в это суть DI.

    > Repository же в основе своей очень прост.
    Скорее всего проблемы с ним возникают из-за недопонимания его предназначения.

    Я думаю, что проблемы с ним возникают, когда в репозитории становится слишком много методов и его ответственность разрастается. Тоже самое происходит с сервисами, которые надо разбивать на команды.

    > Ведь корни его лежат в Java, где никаких IQueriable не было. А теперь люди пытаются мешать совершенно разные парадигмы.

    Да, в этом что-то есть. IQueryable очень сильно раздвинул границы.

    ОтветитьУдалить
  97. @Gengzu

    > Подводя итоги сегодняшнего вечера, можно сделать вывод, что, вместо одного GenericRepository и 20 Specification мы имеем 25 QueryObject'ов. Осталось понять чем это помогает)

    Во-первых, уже отсюда понятно, что репозиторий не нужен. Во-вторых, если у вас действительно только спецификации и репозиторий с одним публичным методом, то всё хорошо. Другой вопрос, что делать с репозиториями в "обычном" их понимании. @Idsa уже приводил пример такой реализации http://pastebin.com/guwfbrgv

    Может для вас это будет шок, но 99% репозиториев выглядят именно так. Поэтому решением является разбить такой singltone-repository на небольшие объекты.

    > Понимаю чем удобно, а так же опасно, протаскивать IQueriable. Но в данном случае мы боремся с ветряными мельницами.

    Я думаю, что удобство/опасность балансируют в зависимости от: длительности проекта, необходимости внесения изменений, текучки в команде, уровня команды и др. факторов. Что скажете?

    ОтветитьУдалить
  98. Александр, не подскажешь как связываются UnitOfWorkFactory и QueryFactory в AccountController. Не совсем ясно, как сущности выдаваемые из Find*Query будут отслеживаться UoF для последующего Commit(). По предлагаемому коде не видно как ILinqProvider попадает в QueryFactory.

    Нет ли где-то этого кода в твоем публичном SVN репозитории, чтобы посмотреть?
    Спасибо!

    ОтветитьУдалить
  99. @ALive

    Примерный код есть здесь http://code.google.com/p/nhibernate2-unitofwork

    Если вкратце, то в случае с NH эти два объекта оборачивают объект Session.

    ОтветитьУдалить
  100. > Я думаю, что проблемы с ним возникают, когда в репозитории становится слишком много методов и его ответственность разрастается. Тоже самое происходит с сервисами, которые надо разбивать на команды.

    Слишком много методов в нём возникает как раз из-за недопонимания его предназначения.
    Я уже пытался акцентировать внимание, что
    GenericRepository + Specification имеет лишь 5 методов. больше не нужно.
    по функциональности получается равноценный вариант с IQuery, где спецификации еще и можно наследовать.

    > Другой вопрос, что делать с репозиториями в "обычном" их понимании. @Idsa уже приводил пример такой реализации http://pastebin.com/guwfbrgv

    > Может для вас это будет шок, но 99% репозиториев выглядят именно так. Поэтому решением является разбить такой singltone-repository на небольшие объекты.

    Не нужно делать такие репозитории. А главное, не нужно пихать их туда, где они не нужны. Веб-сайты тому пример.

    > Я думаю, что удобство/опасность балансируют в зависимости от: длительности проекта, необходимости внесения изменений, текучки в команде, уровня команды и др. факторов. Что скажете?

    Само собой. в комманде из 1го человека, на проекте в 1 человеко месяц имхо допустимо использование IQueriable прямо в контроллере)

    ОтветитьУдалить
  101. @Gengzu

    Мы делаем системы по автоматизации бизнес-процессов через web-интерфейс на ASP.NET MVC. Длительность последнего проекта 10 месяцев командой разработчиков.

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

    ОтветитьУдалить
  102. веб-интерфейс к бизнесс приложению и веб-сайт - разные вещи)

    и всё же, чем вышеописанный подход лучше репозитория со спецификациями?

    ОтветитьУдалить
  103. @Gengzu

    При правильной реализации спецификаций + репозиторий только тем, что нет репозитория.

    Еще этот подход на много легче реализовать, чем специфицации.

    Представьте себе человека, который не слышал ни про репозиторий, ни про Query.

    Он наверняка быстрее освоит создание маленьких объектов, чем создание спецификаций, которые надо передавать в репозиторий.

    ОтветитьУдалить
  104. Возможно. Спорить не стану.

    Как по мне, так спецификация более атомарна, что ли.

    В отличии от Query, которые одновременно и содержит данные и использует их, в одном месте.

    Да и использовать мне кажется это менее удобно.

    Плюс спецификации можно использовать для валидации бизнесс-сущностей, что даёт им дополнительное преимущество :-)

    ОтветитьУдалить
  105. Для начала - спасибо за пост и за развившуюся любопытную дискуссию :)

    > но 99% репозиториев выглядят именно так. Поэтому решением является разбить такой singltone-repository на небольшие объекты.

    Александр, так мы говорим о рефакторинге существующих проектов, или об архитектуре, как она должна выглядеть при проектировании с нуля?

    В примерах по @Idsa репозитарий остается, но они не "по одному на каждую сущность" - а один абстрактный.
    Трудоемкость на добавление запроса в случае спецификаций имхо ниже.
    Вливание в проект новых участников не выше - спецификация еще более стандартный шаблон, чем CQRS, если спецификации на linq - то проблем вообще нет. Объяснить человеку (на существующих примерах), что спеки надо передавать в репозиторий - это не большая проблема, чем обосновать, почему на каждый запрос нужен отдельный класс. В любом случае объяснение - вопрос 15 минут, время привыкания - одинаково.
    При условии "внешних"(IQueryable) наложений инклюдов-сортировок-лимитов число спецификаций очевидно ниже, чем классов в CQS.


    В чем оставшиеся преимущества CQS?
    Dependency-injection? Так мы получаем query строго заточенные под единственное использование - в том месте, куда он инъектируется. А всё-таки основной плюс инверсии зависимостей в выделении сервисов-операций, которые хотя бы теоретически могут быть переиспользуемы.

    На CQS проще накладывается cross-cutting логика типа кэширования. Но тем самым мы только усугубляем заточенность под единственное использование - если в некоторых вью допустимо кэширование, то при некоторой бизнес-логике может быть просто необходимо использовать актуальные данные.

    > При правильной реализации спецификаций + репозиторий только тем, что нет репозитория.
    С учетом этой фразы моя аргументация несколько теряет смысл, да :) Могу только извиниться за поздное вливание в обсуждение :)

    ОтветитьУдалить
  106. @Shaddix, отличное summary!

    Справедливости ради, отмечу, что приведенный репозиторий не глобальный - это репозиторий для тэгов (все запросы там выполняются относительно ctx.BaseTags). А ItemTag и др. - наследники BaseTag.

    В качестве подтверждения привожу ссылку на папку Repositories в svn, где можно найти еще несколько десятков подобных репозиториев: http://code.google.com/p/thingface/source/browse/#svn%2Ftrunk%2FData%2FRepositories

    ОтветитьУдалить
  107. @Shaddix

    > Александр, так мы говорим о рефакторинге существующих проектов, или об архитектуре, как она должна выглядеть при проектировании с нуля?

    И первое, и второе.

    > В чем оставшиеся преимущества CQS?
    Dependency-injection? Так мы получаем query строго заточенные под единственное использование - в том месте, куда он инъектируется.

    Сами Query никуда не инжектируются, инжектируется только IQueryFactory. Остальной проект не знает ни про какие *Query, это в отличие от спецификаций и репозитория.

    > если в некоторых вью допустимо кэширование, то при некоторой бизнес-логике может быть просто необходимо использовать актуальные данные.

    В чем проблема?

    > С учетом этой фразы моя аргументация несколько теряет смысл, да :) Могу только извиниться за поздное вливание в обсуждение :)

    Продолжаем обсуждение :) Истина где-то рядом.

    ОтветитьУдалить
  108. @Idsa

    Спасибо за ссылки! Узнаю наши репозитории, такими они были, пока их не покрамсали на Query :)

    ОтветитьУдалить
  109. Здесь уже приводили код для Query, для классических репозиториев (будь они неладны), но не было кода для IQueryable-based репозиториев.

    Вот ссылочка на проект с репозиторием и спецификациями, который я начал разрабатывать уже после того, как перешел на IQueryable: ссылка

    ОтветитьУдалить
  110. @Idsa

    Спасибо за примеры, вы привели их уже довольно много.

    У меня есть несколько вопросов/замечаний по поводу этой реализации.

    В объекте Repository меня смущает метод GetAll (зачем он нужен?), и методы UnitOfWork - Add и SaveChanges.

    И еще не понятно, почему спецификации - это статические методы? Могут ли спецификации накладываться друг на друга?

    ОтветитьУдалить
  111. @Александр

    Хорошие вопросы.

    1. Если сделать Find Usages по методу GetAll, то найдется одно использование в админке: там действительно нужно получить все объекты данного типа.

    Как альтернатива этому решению - сделать TrueSpecification, и накладывать ее, когда нужно выбрать все сущности. Учитывая, что GetAll будет использоваться крайне редко, пожалуй, решение с TrueSpecification было бы правильнее. Зачекинил это решение вместо GetAll.

    2. Add и SaveChanges.

    SaveChanges - да, нужно вынести в UnitOfWork. Иначе глупо получается, работаешь с несколькими репозиториями (в рамках одного контекста, сессии и т. д.), а SaveChanges вызываешь только у одно (причем любого) из них.

    Почему я этого еще не сделал? Да потому что метод SaveChanges пока не используется. Если заглянуть в код, например, класса BattlesService, то можно увидеть, что там используется ContextScope - самописный аналог TransactionScope.

    Но это не отменяет того, что SaveChanges нужно выносить из базового репозитория. А вот об Add я бы этого не сказал. Мне кажется, ему самое место в репозитории. Или нет?

    Для работы со спецификациями я использую библиотеку <a href="http://linqspecs.codeplex.com/>LinqSpecs</a>. Класс AdHocSpecification перегружает операции и позволяет накладывать спецификации друг на друга.

    Почему статические? Хм... Статические удобнее использовать, и при этом я пока не заметил недостатков у этого подхода. По крайней мере статические спецификации никак не ограничивают тестируемость репозиториев (могу привести пример, подтверждающий эту мысль).

    ОтветитьУдалить
  112. > А вот об Add я бы этого не сказал. Мне кажется, ему самое место в репозитории. Или нет?

    Мне как-то больше нравится вот такой подход P of EAA Catalog | UnitOfWork

    ОтветитьУдалить
  113. Хм... На текущий момент мое понимание взаимоотношений между UnitOfWork и репозиториями заключается в том, что репозиторий управляет entity-specific операциями (Add, Delete), а UnitOfWork управляет операциями по контексту, сессии в целом (SaveChanges, ClearChanges).

    Хотя, с другой стороны, Add и Delete модифицируют UnitOfWork. И если бы не было прослойки в виде EF контекста, такие методы пришлось бы явно вызывать через UnitOfWork.

    Я в сметении :)

    ОтветитьУдалить
  114. Мысли вслух. То, что нынче именуют UnitOfWork в сравнении с фаулеровским UnitOfWork, скорее, нужно называть UnitOfWorkManager, потому что настоящий UnitOfWork встроен в ORM.

    ОтветитьУдалить
  115. Если по существу, то мне не нравится, что для того чтобы выполнить запрос

    .Where(x => x.Email == email) .SingleOrDefault();

    мне требуется написать:
    1. целый класс FindAccountByEmailQuery
    2. метод в интерфейс IQueryFactory
    3. метод в класс QueryFactory

    и протащить параметр email через все эти "слои".

    Разве не очевидно дублирование кода и что при любом изменении придется править в трех местах?

    ОтветитьУдалить
  116. @Nikolay Sergeev

    Потрудитесь прочитать комментарии выше.

    QueryFactory - это одна из возможных реализаций идеи. Можно делать тоже самое вообще не реализуя IQueryFactory.

    Идея в том, что надо избавиться он больших объектов Repository, которые становятся фактически Singletone'ми.

    ОтветитьУдалить
  117. Александр, очень хорошая статья, спасибо. Я сейчас как раз столкнулся со стремительным разрастанием репозиториев в проекте. Мало того, у нескольких (не всех) репозиториев может присутствовать общая логика. В этом случае наследование применять как-то совсем не хочется. Рефакторинг инфраструктуры сейчас мне представляется как раз в выделении методов репозиториев в отдельные Query объекты.

    Не очень понятным осталось только одно: как вы предлагаете реализовывать запросы на сохранение/модификацию domain-сущностей?

    ОтветитьУдалить
  118. @Mr Fontaine

    Сохранением и модификацией занимается UnitOfWork, который автоматически отслеживает состояние объектов.

    Можете посмотреть для примера http://blog.byndyu.ru/2010/07/2-unit-of-work_10.html

    ОтветитьУдалить
  119. Александр, конкретно в моем случае приходится работать с Ado.net (реализация конкретных репозиториев построено на Ado.net). То есть я только лишь собственными силами создаю некое подобие ОРМ. Использовать DataContext из LinqToSql или Session из NHypernate я не могу (таковы условия задачи). Реализация же самостоятельно с нуля IUnitOfWork представляется мне довольно таки трудоемкой задачей. Интересно было бы услышать ваше мнение на этот счет.

    ОтветитьУдалить
  120. @Mr Fontaine

    Возможно вам больше подойдет шаблон ActiveRecord. Могу посоветовать полистать книжку, возможно найдете решение близкой к своей проблеме http://martinfowler.com/books.html#eaa

    Есть вариант посмотреть исходники NHibernate и сделать свой велосипед ля ADO.NET.

    Ну и наконец перейти на нормальную ORM со временем.

    ОтветитьУдалить
  121. Артур Терегулов23 июля 2012 г., 16:45

    сделал статический репозиторий, потом два дня пытался выловить баг - данные кэшировались

    ОтветитьУдалить
  122. Блин я студент фита, работаю, сейчас в отпуске, перечитал весь ваш блог. столько нового и полезного за все время обучения в вузе не видел. Выйду с отпуска с таким багажом знаний коллеги не узнают меня) Спасибо вам.

    ОтветитьУдалить
  123. Спасибо за поддержку.

    ОтветитьУдалить
  124. Возможно я не все понял. Ну не получается ли у нас при таком подходе 
    QueryFactory становится God Объектом? И не получатеся ли у нас что "ФабрикаЗапростов" это почти тот же самый Репозиторий только  с использование шаблона Specification( ILinqProvider)?

    p.s. Я не в коем случаи не придираюсь. Просто хочу разобраться, т.к. остаються непонятки, а опыта мало.

    ОтветитьУдалить
  125. Возможно я не все понял. Ну не получается ли у нас при таком подходе QueryFactory становится God Объектом? И не получатеся ли у нас что "ФабрикаЗапростов" это почти тот же самый Репозиторий только  с использование шаблона Specification( ILinqProvider)?
    p.s. Я не в коем случаи не придираюсь. Просто хочу разобраться, т.к. остаються непонятки, а опыта мало.

    ОтветитьУдалить
  126. Артем, 2 момента:
    1. QueryFactory не содержит кода, поэтому по определению не может быть God Object. В ней нет никакой логики
    2. Эта фабрика приведена для примера, посмотрите во что она может превратиться http://blog.byndyu.ru/2011/08/queryfactory-iqueryfactory.html

    ОтветитьУдалить
  127. Добрый день !
    У меня вопрос про то, как тестировать ваш класс ...Query.
    Первый же рефакторинг показал, что этот класс можно наследовать от интерфейса
    public interface IFindGroupByRespUser where T : class
    {
    IQueryable Execute();
    }

    и в итоге класс Query может иметь такую реализацию

    public class FindGroupByRespUser : IFindGroupByRespUser
    {
    private readonly IBaseRepositoryUnit Provider;
    private int repsId;


    public FindGroupByRespUser(IBaseRepositoryUnit provider, int rId)
    {
    Provider = provider;
    repsId = rId;
    }
    public virtual IQueryable Execute()
    {
    return Provider.FindListQ(g => g.ResponsibleUserId == repsId);
    }
    }
    }
    вопрос про тестирование метода Execute такого класса - вот такой тест проходит
    [TestMethod]
    public void ExecuteTest3()
    {
    var query= Mock.Of>(t=> t.Execute()== new EnumerableQuery(
    new List()
    {
    new tOperGroup() { OperGroupId = 12}
    }
    ));

    var result = query.Execute().ToList();
    Assert.AreEqual(result[0].OperGroupId, 12);
    }
    но он тестирует вызвов метода Execute реализации интерфеса IFindGroupByRespUser, а вот как протестировать вызов метода Execute для реализации FindGroupByRespUser ?
    вот тест
    [TestMethod]
    public void FindGroupByRespUserQuery()
    {
    var repoMock = new Mock>();

    repoMock.Setup(e => e.FindListQ(t => t.OperGroupId == 12))
    .Returns(t => new EnumerableQuery(
    new List()
    {
    new tOperGroup() { OperGroupId = 12}
    }
    ));


    FindGroupByRespUser query2 = new FindGroupByRespUser(repoMock.Object, 12 );

    var r = query2.Execute();
    Assert.AreEqual(r.Count(), 1);


    }
    не срабатывает, так как moq реализация репозитория не возвращает то, что было ей указано.
    а хотелось бы тестировать реализации объектов Query конкретных классов, а не сконструированных через moq.

    ОтветитьУдалить
  128. Подскажите, что вы ожидаете от теста? Вы хотите понять правильно ли работает лямбда-выражение?

    ОтветитьУдалить
  129. то, что при наличии в репозитории только одного объекта метод Excecute вернет набор объектов только с одним элементом.
    хотя бы так.
    можно усилить - при наличии в репозитории объекта со значением поля 12 возвращается объект с таким же значением поля.
    но Moq объект для репозитория не срабатывает, когда он передается в качестве реализации интерфейса в класс Query

    ОтветитьУдалить
  130. Подскажите, у вас есть интерфейс IBaseRepositoryUnit, что за метод FindListQ?

    ОтветитьУдалить
  131. по заданному Expression возвращает набор объектов
    реализация может быть такой
    public virtual IQueryable FindListQ(Expression> predicate)
    {
    return Query(predicate);
    }
    вызываться может так
    var list = repository.FindListQ(t => t.OperGroupId == 12);

    ОтветитьУдалить
  132. В этом случае проще всего использовать Stub. Вот пример http://pastebin.com/rVwx4VJN

    ОтветитьУдалить
  133. спасибо большое - я к таким же выводам пришел.

    ОтветитьУдалить
  134. Почему Вы называете QueryFactory фабрикой, если он не возвращает объектов query? В данном случае это скорее фасад с инжекцией зависимости, т.к. он возвращает результат выполнения query а не сам query...

    ОтветитьУдалить
  135. Довольно интересная статья, а ещё бы побольше статей на тему DOM & JQUERY или DOM & JAVASCRIPT. Хочу написать свой сайт и парсить туда всякую информацию. В интернете уже столько доменов появилось что не знаешь какое имя зарегистрироваться. Недавно пробовал открыть что нибудь связанное с едой и взял имя с одной дорамы Хупэпт ну или repept в результате выяснилось что домен то http://repept.ru уже занят и пришлось сделать какой то kolbasotes. А вообще подскажите сложно ли написать свой грабер статей или нет ? Хочу с этом сайта всё собрать что бы ему не повадно было, а то обидно.

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