Как уже обсуждалось в 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 и концепцию решения.
Ссылки
Repository is the new Singleton
DDD : Command Query Separation as an Architectural Concept
Query Objects vs Methods on a Repository
Выглядит достаточно перекомпилкейчено. У нас IQueryFactory просто возвращает IQueriable, екстеншен методы уже делают то что у вас делают Query.
ОтветитьУдалить@Mike Chaliy
ОтветитьУдалитьМожет появится соблазн наращивать IQueriable, где не надо.
С разделением запросов на объекты - ок, но тебе не кажется, что QueryFactory у тебя со временем и будет тем самым Repository с кучей методов?
ОтветитьУдалитьМожно, конечно, добавить в фабрику метод Create(..), но и здесь потенциально возможны проблемы.
@Sergun
ОтветитьУдалитьВ QueryFactory нет логики.
>Какой репозиторий использовать, если метод >выборки работает с несколькими сущностями?
ОтветитьУдалитьзависит от того, в контексте какого агрегата идет работа.
Пощупать код на эту тему можно тут https://github.com/xelibrion/Lab/tree/master/QueryObject
ОтветитьУдалить@xelibrion
ОтветитьУдалитьНа счет агрегата соглашусь.
Спасибо за ссылку на код.
>>Можно из одного репозитория вызывать другой, но это превратит код в паутину. Можно один репозиторий унаследовать от другого, но это приведет к огромным God-object'ам.
ОтветитьУдалитьА можно выделить отдельную зависимость, как и в случае с любой дублирующейся логикой
>>Тогда логика построения запроса и его выполнения будет разбросана по коду.
Я с момента обсуждения IQueryable/IEnumerable репозиториев продал душу дьяволу и стал использовать IQueryable-репозитории. Правда, следую несколькоим правилам. Вся логика инкапсулируется: в частности условия накладываются только через спецификации, а за пределами репозитория я позволяю себе наращивать IQueryable только View-specific методами: сортировка, выборка и т. д. Пока меня устраивает.
Query выглядит интересной альтернативой Repository, но при этом какой-то чересчур многобуквенной, что ли. Да и к тому же, по сути, это обертка над одним методом классического репозитория с вытекающим отсутствием гибкости.
Паттерн репозитория предназначен для решения следующей проблемы - у вас в проекте большая и сложная бизнсе-модель, запросы к бизнес-сущностям разбросаны по всему проекту, при этом появляется куча дублированного кода.
ОтветитьУдалитьМы используем репозитории для следующих задач:
1. решение проблем с дублированием кода
2. организация кода
3. юнит-тестирование
Вот мой взгляд на использование репозитриев:
>> реализация шаблона Repository фактически превращается в статический класс
т.к. репозиторий не содержит состояние, то можно использовать и статический класс, но, по мне так, это есть не есть хорошо, особенно для юнит-тестирования
>>Какой репозиторий использовать, если метод выборки работает с несколькими сущностями? Если нам надо выбрать Account по Project, то мы пойдем за этим методом в AccountRepository или ProjectRepository?
конечно AccountRepository, а токак то не звучит - из хранилища (repository) проектов мы вытягиваем акаунты
>>Что делать, если разные репозитории должны использовать закрытые методы друг друга?
уникать этой ситуации, честно говоря я не могу придумать реальный пример, когда это действительно надо
>>Может использовать вообще один репозиторий на весь проект?
лучше отказаться от репозиториев и использовать QueryBuilder паттерн, т.к. скорей всего в вашем проекте не более 20 бизнес-сущностей
>>Может репозиторий - это просто построитель запросов?
из названия понятно, что это хранилище бизнес-сущностей, реализация которого инкапсулирует запросы к мапперу
>>Надо ли дублировать методы в сервисе и репозитории?
т.к. репозиторий относится к слою предметной области, то почему бы и нет?
>> В QueryFactory нет логики.
ОтветитьУдалитьНо при этом ты будешь вынужден изменять ее всякий раз, когда добавляется новый специфичный запрос.
>>Но при этом ты будешь вынужден изменять ее всякий раз, когда добавляется новый специфичный запрос.
ОтветитьУдалитьДа и к тому же это, по сути, ServiceLocator. И чтобы понять, что нужно застабить, нужно лезть в код (из конструктора не понятно)
С запросом суть понятна. А как предлагаешь выполнять команды (запись)?
ОтветитьУдалитьПисать целый класс ради строки
ОтветитьУдалить.Where(x => x.Email == email)
.SingleOrDefault();
Очень смешно, если честно :-)
@Nikolay Sergeev, а что с этим не так?
ОтветитьУдалить>> @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 не добавляются.
@Idsa
ОтветитьУдалить> А можно выделить отдельную зависимость, как и в случае с любой дублирующейся логикой
Например?
> Query выглядит интересной альтернативой Repository
А вы попробуйте хотя бы на тестовом проекте, иначе преимущества не так очевидны.
@agat
ОтветитьУдалить> т.к. репозиторий не содержит состояние, то можно использовать и статический класс, но, по мне так, это есть не есть хорошо, особенно для юнит-тестирования
Да, это еще вредит DIP и затрудняет инжектирование репозитория в другие объекты.
> уникать этой ситуации, честно говоря я не могу придумать реальный пример, когда это действительно надо
Если внутри одного репозитория есть логика выборки, которая должна повториться в другом репозитории. Например, выбор активных пользователей.
> лучше отказаться от репозиториев и использовать QueryBuilder паттерн, т.к. скорей всего в вашем проекте не более 20 бизнес-сущностей
В нашем проекте гораздо больше 20.
> из названия понятно, что это хранилище бизнес-сущностей, реализация которого инкапсулирует запросы к мапперу
Посмотрите комментарии к статье http://blog.byndyu.ru/2011/01/domain-driven-design-repository.html и удивитесь сколько есть мнений на этот счет :)
> т.к. репозиторий относится к слою предметной области, то почему бы и нет?
Потому что сервит превращается во второй репозиторий. Если в репозитории метод называется GetActiveAccounts, то в сервисе появится FindActiveAccounts с одной строчкой вызовом метода репозитория. Это становится излишним, когда повтояется по 40-50 методов.
@Sergey Zwezdin
ОтветитьУдалитьНу да, это обычная фабрика. Можно разруливать все зависимости автоматически через байдинг в IoC контейнере. Для этого надо использовать конвенции вместо конфигурации. Тоже вариант.
@Idsa
ОтветитьУдалить> Да и к тому же это, по сути, ServiceLocator. И чтобы понять, что нужно застабить, нужно лезть в код (из конструктора не понятно)
Можно пример кода? Лучше через http://pastebin.com
@Алексей Романовский
ОтветитьУдалитьКоманды, которые C-QRS, в MVC мы делаем через FormHandler'ы. Хотел дать ссылку на какую-нибудь статью про эту тему, но почему-то не нашел :)
Либо еще поищу и дам ссылку, либо напишу про это.
@Pasha
ОтветитьУдалитьСпасибо за вопрос, видимо я не полностью описал преимущества. Кстати, вы почитали ссылки в конце статьи?
Самое главное, что мы уходим от не тестируемых репозиториев к маленьким тестируемым классам с единственной отвественностью и понятной логикой. Фактически облегчаем себе разработку.
Для начала я бы посоветовал вам создать тестовый проект и поразрабатывать его в этом стиле хотя бы несколько вечеров. Тогда вы увидите как легка эта концепция в понимании и расширении системы. Это особенно важно для работы в команде.
> а как же D из soliD?
По-моему нарушения нет. Реализация фабрики скрыта за интерфейсом. Сама фабрика знает про всех, кого произоводит. Но в ней никакой логики, поэтому ничего страшного. Опять же можно использовать конвенции и сделать в фабрике единственный метод, который будет работать с IoC-контейнером.
>>Хехе, 20% твоей души - мои
ОтветитьУдалитьАпостол Павел? :)
>>Еще немного - и ты осознаешь, что на самом деле давно используешь нормальный несамописный QueryObject, но будет уже слишком поздно...
Ну хорошо, пусть будет QueryObject. И что? А почему поздно? До конца света время еще есть
@Nikolay Sergeev
ОтветитьУдалитьПисать целый комментарий ради одной строки - очень смешно, если честно
@Александр Бындю, @Алексей Романовский
ОтветитьУдалитьВот статья от Jimmy Bogard
http://lostechies.com/jimmybogard/2011/06/22/cleaning-up-posts-in-asp-net-mvc/
У нас примерно так.
@hazzik
ОтветитьУдалитьСпасибо за ссылку. Еще была статья с названием типа "Посадите ваши контроллеры на диету". Кто найдет, дайте пожалуйста ссылочку.
Это не статья, это презентация Богарда на MVCConf. В самой презентации мало слайдов. Код и презентация вот тут: http://headspringlabs.codeplex.com/SourceControl/changeset/view/936eb41fbdb4
ОтветитьУдалить@Александр Бындю, http://blog.alagad.com/2007/04/18/put-your-controllers-on-a-diet-use-a-service-layer/
ОтветитьУдалить@hazzik @Idsa
ОтветитьУдалитьСпасибо, очень полезные ссылки!
Выглядит, как будто QueryFactory превратился в один большой репозиторий. Саня, если не заставлять меня тратить несколько вечеров на тестовые проекты, в чем выгода?
ОтветитьУдалить@Мурадов Мурад
ОтветитьУдалитьВ том, что у тебя было 10 огромных репозиториев с 1000 срок кода в каждом, а стало 50 маленьких объектов, про которые знает только QueryFactory.
Создание нового запрос сводится к созданию маленького класса через TDD. Создать класс *Query очень просто, потому что в нем мало логики. Сравни это с добавлением еще одного метода в огромный репозиторий.
При изменении запроса, мы заходим внутрь одного маленького класса и меняем его, это в отличие от изменения запроса внутри репозитория.
В самой QueryFactory нет никакой логики, в отличие от больших репозиториев. QueryFactory даже тестировать не надо.
Не соглашусь про огромные репозитории. Все зависит от сущностей и их зависимостей, но по крайней мере у меня репозитории огромными не становятся.
ОтветитьУдалитьЕсли будет новый запрос, то в случае с репозиторием будет добавлен только один метод, а для QueryFactory - класс запроса и метод в фабрике.
Тестирование репозитория и множества классов запросов по объему все равно будут равны.
Приятно, конечно, что запрос будет в отдельном классе, но как минимум выгода не очевидна.
Более того, если использовать Castle Windsor, то у QueryFactory даже реализации нет, только интерфейс
ОтветитьУдалить@xelibrion
ОтветитьУдалитьДа, так будет еще проще. В статье я показал суть решения и один из способов реализации.
@Мурадов Мурад
ОтветитьУдалитьЭто до тех пор, пока у тебя маленький проект и в репозиториях по 5-6 методов. Если проект год делает 10 человек, то репозитории становятся ооочень большими :) Представь себе, что у тебя 15-20 агрегатов, т.е. примерно 15-20 репозиториев с кучей методов.
Я могу судить об этой проблеме еще и по письмам, которые мне приходят. Основной вопрос, который задают: "Как справится со сложностью репозиториев/сервисов?"
>> Я могу судить об этой проблеме еще и по
ОтветитьУдалить>> письмам, которые мне приходят. Основной
>> вопрос, который задают: "Как справится со
>> сложностью репозиториев/сервисов?"
Обычно репозитарии становятся большими, когда репозитрии пользуют как слой доступа к данным или рут агрегаты постороенны неправильно. Или когда пытаются через репозитарии читать данные для отображения. Я вполне допускаю что репозитарий может вырасти до больше чем 5-6 методов, но 1) эти методы обычно две три строки 2) это всеже скорее исключение, и не стоит одно исключение решать усложнением всего проекта.
@Александр
ОтветитьУдалить>> Спасибо за вопрос, видимо я не полностью описал преимущества.
Нет, преимущества как раз расписаны хорошо. Проблема в другом - все поднятые тобой начале статьи проблемы связаны с внешним интерфейсом репозитория, с временем его жизни, и со структурой зависимостей остальных классов от репозитория.
А предложенный QueryFactory - это реализация того же самого внешнего интерфейса IRepository через делегирование. Да, хорошая реализация. В определенных обстоятельствах - лучше обычной пачки методов. Решает ли она проблемы внутренней реализации? Возможно. Решает ли она проблемы, поднятые в начале статьи - нет.
Задумайтесь, как отразится переход с IRepository на IQueryFactory на остальном проекте (исключая разрезание класса-репозитория на мелкие классы-запросы). Повсюду в коде будет заменен IRepository на IQueryFactory и .. все? Проблема наличия god object решена переименованием интерфейса?
Ты предлагаешь посидеть пару вечеров и попробовать. Ок, взял старый sandbox проект, переписал внутреннюю реализацию репозитория с обертки над DataContext на агрегирование пачки мелких классов-запросов - ок, имеет смысл.
Переименовал везде в проекте IRepository на IQueryFactory, но просветления не почувствовал.
Александр, потрать и ты 15 минут. Возьми текущий проект, переименуй IQueryFactory обратно в IRepository, удали внутреннюю реализацию фабрики/репозитория, вместе с классами-запросами. И спроси другого разработчика, какой паттерн используется - старый плохой репозиторий, или новый хороший QueryFactory. А потом удиви его.
@Александр Бындю, @hazzik
ОтветитьУдалить>Команды, которые C-QRS, в MVC мы делаем через FormHandler'ы. Хотел дать ссылку на какую-нибудь статью про эту тему, но почему-то не нашел :)
>Самым простым и рабочим решением является разделение репозиториев на небольшие объект-запросы.
Спасибо за ссылки. Я знаком с CQRS в MVC.
Тогда так задам вопрос: обычно в хэндлерах для записи данных используется репозиторий. Если у нас теперь есть только объекты-запросы, то кто будет заниматься сохранением данных?
Подразумевается, что репозиторий разделяется также и на объекты-"команды"?
@Mike Chaliy
ОтветитьУдалитьДопустим у нас 5 агрегатов на весь проект. Тогда на каждый агрегат может приходится по 20-30 разных запросов. Это как раз и есть размер репозитория.
А переход на Query это совсем не усложнение проекта, вы попробуйте.
@Pasha
ОтветитьУдалить> Повсюду в коде будет заменен IRepository на IQueryFactory
Я бы так сказал. Все I*Repository (IProductRepository, IAccountRepository и т.д.) будут заменены на IQueryFactory.
> Проблема наличия god object решена переименованием интерфейса?
Проблема решена разделением ответственности на маленькие объекты. Это понятно?
@Алексей Романовский
ОтветитьУдалитьЯ же попросил прочитать ссылки после статьи. Вы это видимо не сделали и решили сразу позадавать вопросы :)
Ну ладно, вот эта ссылка http://richarddingwall.name/2009/10/22/repositories-dont-have-save-methods
Сохранением объектов занимается UnitOfWork.
@Александр Бындю
ОтветитьУдалитьВ http://blog.byndyu.ru/2010/07/2-unit-of-work_10.html ты рассказывал про UnitOfWork и показывал там примеры интерфейса IUnitOfWork с Commit и Rollback. Правильно ли я сейчас понял, что IUnitOfWork нужно расширить и реализовать еще методы, например, Save(object) и Delete(object), с помощью которых и будут сохраняться и удаляться объекты?
Если все так, то все встает на свои места!
@Алексей Романовский
ОтветитьУдалитьТочно! :) Вот такой UoW http://martinfowler.com/eaaCatalog/unitOfWork.html
Только у нас нет метода Delete, он плохо влияет на производительность БД. При удалении строки СУБД приходится перестраивать индексы, каскадно удалять сущности если необходимо. Проще делать объекты IsDeleted. Но это уже по выбору.
@Александр Бындю
ОтветитьУдалитьСпасибо! У меня прямо-таки пазл сложился! Я в восторге! Я как-то однобоко смотрел на UoW.
Теперь надобность в репозиториях отпадает напрочь. Действительно очень простое, красивое и аккуратное решение получилось.
@Алексей Романовский
ОтветитьУдалитьВы попробуйте в проекте, если возникнут вопросы/проблемы, то будем вместе решать. Можно здесь http://groups.google.ru/group/dotnetconf
@Александр:
ОтветитьУдалитьПочему бы не возвращать интерфейс 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).
Просто, как я понял, в статье решается проблема декомпозиции отдельных репозиториев с одновременной композицией всех репозиториев в один класс. Это наверное не то, что мы все ждали в рассказе про то, как вы делаете CQRS ))
ОтветитьУдалитьВсе generic'и съело.
ОтветитьУдалитьhttp://pastebin.com/jZdad3q1
ОтветитьУдалить>> Проблема решена разделением ответственности на маленькие объекты. Это понятно?
ОтветитьУдалитьДа. Понятно. Решена проблема внутренней организации репозитория.
Проблемы, поднятые в начале статьи - не решены [если в результате свести все в QueryFactory]. Это понятно?
Я честно попытался последовать вашему совету. Применил подход из статьи к своему текущему проекту. Судя по комментариям - я вообще единственный, кто это сделал. Честно описал полученный резульат в комменте на хабре, стараясь не устраивать полотно в ответе. Получил 3 минуса в карму :) Отличная благодарность :)
@Nikita Govorov
ОтветитьУдалитьСпасибо за подробный комментарий.
Я лично за то, чтобы запросы строились на LINQ, мне кажется это избавляет от лишней работы, но если вам проще было сделать свой язык запросов, то тоже вариант.
Самое главное, что вы строите запрос прямо в контроллере и этот метод фактически не инкапсулирует логику запроса где-либо. Я могу построить запрос в котроллере или в сервисе. При этом часть логики запросов обязательно начнет дублироваться.
А вы использовали описанный вами подход?
> Просто, как я понял, в статье решается проблема декомпозиции отдельных репозиториев с одновременной композицией всех репозиториев в один класс.
Нет, QueryFactory не делает запросы в отличие от Repository, а только создает объекты, т.е. являет обычной фабрикой объектов. Сам код репозиториев теперь разбит на маленькие *Query.
> если в результате свести все в QueryFactory
ОтветитьУдалитьЕще раз. QueryFactory не имеет никакой логики. Посмотрите внимательно. QueryFactory не строит запросы, не делает запросы, не сохраняет и не выбирает данные - это фабрика объектов и только один из возможных вариантов работы с объектами типа *Query.
> Получил 3 минуса в карму :)
Ну это же Хабр. Я вот тоже в карму получил минус и ни одного плюса :)Даже не хочу спрашивать о причине.
>> Сам код репозиториев теперь разбит на маленькие *Query.
ОтветитьУдалитьhttp://martinfowler.com/eaaCatalog/repository.html
сам код репозиториев должен быть разбит на маленькие inMemoryStrategy и при реализации классического паттерна. Чем ваш подход отличается?
Еще раз. Классический репозиторий не имеет никакой логики. Посмотрите внимательно. Репозиторий не строит запросы, не делает запросы, не сохраняет и не выбирает данные - это фабрика объектов и только один из возможных вариантов работы с объектами типа *inMemoryStrategy. Хм.
@Pasha
ОтветитьУдалить> сам код репозиториев должен быть разбит на маленькие inMemoryStrategy
Это откуда вы взяли?
А вот из определения:
> where query construction code is concentrated...adding this layer helps minimize duplicate query logic
Ну дак есть там работа с запросами или нет?
И самое главное - Repository не нужен.
Сохранять и удалять объекты нужно в UnitOfWork, выбирать данные с помощью *Query, изменять данные с помощью команд.
>> Это откуда вы взяли?
ОтветитьУдалитьа откуда вы взяли что не должен?
Посмотрите на диаграмму в PoEAA, по ссылке. там же черным по белому нарисовано, в каком месте критерий (не запрос) превращается в коллекцию объектов people who satisfied the criteria. это не в коде репозитория происходит.
>> Ну дак есть там работа с запросами или нет?
нет, там есть код типа new SomeQuery(). не надо понимать текст буквально как "прямо в теле репозитория должен быть код запросов".
> Посмотрите на диаграмму в PoEAA, по ссылке. там же черным по белому нарисовано, в каком месте критерий (не запрос) превращается в коллекцию объектов
ОтветитьУдалитьНу это можно записать так:
class AccountRepository
{
public IList GetAccount()
{
return linqProvider.Query().Where(x => x.Id == 10);
}
}
Вот вам и inMemoryStrategy на LINQ. Можно в него и спецификации передавать и еще много всего можно. Я просто не понял, почему вы так критично собрались разбивать его на стратегии.
> прямо в теле репозитория должен быть код запросов
Вот вам код на LINQ внутри репозитория.
И самое главное - Repository не нужен.
>> Ну это можно записать так
ОтветитьУдалитьа можно так:
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.
да, такой репозиторий - не нужен. вот только он такой же и внутри и снаружи, как ваша фабрика.
@Pasha
ОтветитьУдалитьЭта статья для тех, кто столкнулся с проблемами при использовании шаблона Repository, которые связаны с разрастанием классов и ответственностей. Предлагается убрать Repository и разделить его на объекты запросов. Как вариант можно использовать QueryFactory. Ключевое здесь: "как вариант" = не обязательно. Мой коллега в ближайшее время собирается выложить пост, где нет QueryFactory.
Если у вас есть свой подход, то я могу только порадоваться.
@Александр:
ОтветитьУдалить>Я лично за то, чтобы запросы строились на LINQ, мне кажется это...
Это уже зависит от реализации запроса(супер-типа для этого запроса). У меня есть запросы и с HQL и даже с SQL внутри, linq и QueryOver(Criteria) - это вообще обычное дело.
>Самое главное, что вы строите запрос прямо...
Не всегда:
>При этом может существовать спец. запрос, семантика которого, закладывается в названии: IGoldAccountQuery
Я возвращаю QueryObject в общими доп. возможностями необх. для буквы Q, а не сразу результат запроса как Вы.
Это дает возможность дать операционному слою те возможности, в которых он действительно нуждается.
>А вы использовали описанный вами подход?
Да конечно.
>Нет, QueryFactory не делает запросы в отличие...
Вы разбиваете методы репозитория на отдельные классы(*Query) и объединяете все репозитории(IProductRepository, ICustomerRepository etc) в один объект(пусть даже он не содержит логики) QueryFactory.
Я это имел ввиду.
Если смотреть на запрос не с точки зрения внутреннего устройства, а с точки зрения потребителя, то он должен предоставлять возможность наращивания,
иначе легче вызвать метод репозитория. Вы же поменяли внутренние понятия, но для потребителя вместо гранулированных по агрегатам (фун. понятиям и прочему) интерфейсов
репозиториев, предоставляете все те же методы сваленные в один объект(удобно инжектить, но не удобно искать). Посмотрите на ваш подход со стороны потребителя
(разработчика опер. слоя или слоя домена), в чем выгода? Меньше инъекций? Так, как я писал выше, их вообще можно убрать. Может быть стоит поискать ответы на ваши вопросы
к проблемному репозиторию в другом? Вы произвели декомпозицию огромных репозиториев, ну и все, пусть IProductRepository, ICustomerRepository etc остаются, в них тоже не будет
логики, но для потребителя здесь не появляется QueryObject - это просто организация кода в пределах слоя доступа к данным основанного на репозиториях,
и при этом не теряется его грануляция.
@Александр. Статья хорошая. Меня смущает что во второй части вы предлагаете репозиторий вернуть, но под другим названием.
ОтветитьУдалитьКак вариант - хорошее слово. Но вы же этот вариант настойчиво рекомендуете.
Обязательно прочитаю следующую статью.
@Nikita Govorov
ОтветитьУдалитьСогласен с тем, что вы написали.
> Вы произвели декомпозицию огромных репозиториев, ну и все, пусть IProductRepository, ICustomerRepository etc остаются
Да, как вариант.
@Pasha
ОтветитьУдалитьКак я уже писал, главное - разбить большие репозитории на небольшие объекты.
Если QueryFactory будет возвращать IQuery (как сделано у нас в проекте), то можно будет наращивать запросы, что удовлетворит @Nikita Govorov, т.к. QueryFactory будет фабрикой запросов :)
>> Если QueryFactory будет возвращать IQuery (как сделано у нас в проекте)
ОтветитьУдалитьЭто большое если. у нас репозиторий возвращает IQueryable. Т.е. репозиторием он называется только потому, что выполняет ту же задачу что репозиторий у Фаулера. Настощий репозиторий - IQueryProvider, но все разработчики принимают эту абстракцию как должное.
@Pasha
ОтветитьУдалитьЯ лично против того, чтобы репозиторий возвращал IQueryable. Подробно обсуждалось в статье и комментария к посту Domain-Driven Design: Repository
Забавно, что пришли к IQueryable. Ведь в IQueryable-репозиториях проблем с разрастанием не наблюдается (спасибо спецификациям), а значит изначально смысла в создании Query нет (по крайней мере если следовать принципу "главное - разбить большие репозитории на небольшие объекты"
ОтветитьУдалитьПроцитирую из статьи:
ОтветитьУдалить"Тогда логика построения запроса и его выполнения будет разбросана по коду. Например, можно создать часть запроса в сервисе, добавить в него условий в контроллере, а выполнить запрос в модели. Логика в запросах неизбежно начнет дублироваться. К тому же такие «размазанные» запросы будет сложно тестировать."
Александр, а ты не против десятка перегрузок в зависимости от разных OrderBy, Select, Include и т. д.?
ОтветитьУдалить@Idsa
ОтветитьУдалитьПерегрузок чего? Можно хотя бы небольшой пример.
Насчет разбрасывания по коду. Это можно контролировать. Я писал выше, что за пределами репозитория, который накладывает переданную спецификацию, я позволяю себе накладывать только View-specific методы, которые не несут в себе логики. А значит вся логика остается инкапсулированной в спецификации.
ОтветитьУдалитьВот реальный пример классического репозитория: http://pastebin.com/guwfbrgv Говоря о перегрузках, я имею в виду жесть вроде GetTagsWithLocalizations vs. GetTagsWithParentsAndLocalizations
ОтветитьУдалить@Idsa
ОтветитьУдалитьЯ понял, это перегрузки для подкачки связанных коллекций, так?
Если честно, для оптимизации скорости иногда приходится так делать.
Может вы знаете другое решение?
>> Я лично против того, чтобы репозиторий возвращал IQueryable
ОтветитьУдалитьа IQuery можно? судя по названию - этот свой IQueryable. Чем он лучше стандартного?
@Александр,
ОтветитьУдалитьПодкачка значений - это лишь частный случай. Еще могут быть нужны разные сортировки. Или, например, иногда нужно выбрать всю сущность, а иногда лишь одно свойство (тот же айди). Иногда нужно выполнить Single, иногда FirstOrDefault и т. д. И на все это по-хорошему нужно делать свои перегрузки.
Именно насмотревшись вот таких классических репозиториев, я перешагнул через себя (для меня это действительно было сложное решение) и начал использовать IQueryable (с описанными выше ограничениями).
И вот я как представлю, что все эти перегрузки нужно растащить по отдельным классам, сразу энтузиазм по отношению к Query пропадает (Павел привел хороший пример на Хабре, умножив 20 репозиториев на 30 методов и получив 600 Query - впечатляет). Хотя теоретически паттерн интересный, и разного рода сквозная логика на него ложится лучше. Хотя в случае репозиториев похожего эффекта можно добиться дополнительным слоем абстракции в виде сервисов.
Привет, Александр, коллеги!
ОтветитьУдалитьСпасибо за статью!
Вопрос: как все же в приведенном подходе избавитсяь от дублирования кода между классами запросов?
Скажем один из классов-запросов выбирает Accounts по критерию "Пол + год рождения", а в другом случае нужна выборка по критерию "Пол + год рождения + какой-то статус"?
@ALive
ОтветитьУдалитьМы делаем это выделением базового класса. В этом случае наследование оправдано. В случает, когда у репозитория 30 методов, наследование не оправдано.
@Александр Бындю
ОтветитьУдалитьнасчет логики построения запроса и разбрасывания. репозиторий инкапсулирует логику построения запроса к данным в базе. то, что у вас называется Query - это спецификации, запросы на массивами объектов. Репозиторий никак не решает проблему разброса логики построения спецификаций. Он принимает готовые спецификации как параметры, и только умеет их преобразовывать в запросы к хранилищу данных внутри себя.
Как решать проблему разброса логики создания спецификаций? да как угодно, группировать их в SpecificationFactory (QueryFactory) в вашем варианте. Строить спецификации в методах расширения, и группировать в классы по сущностям (pipes and filters).
Как обрабатывать подгрузку коллекций? например добавить механизм interception, и позволить управлять загрузкой явно, из бизнес-логики, через методы расширения.
Но к изначальным проблемам, поднятым в топике, это не имеет никакого отношения.
@Pasha
ОтветитьУдалить> Он принимает готовые спецификации как параметры, и только умеет их преобразовывать в запросы к хранилищу данных внутри себя
Зачем он тогда нужен? Если можно сделать запрос на Linq и ORM его выполнит. Хватит класса Query.
>> Зачем он тогда нужен? Если можно сделать запрос на Linq и ORM его выполнит. Хватит класса Query.
ОтветитьУдалитьСамописный "репозиторий" - только для адаптации ORM к контексту проекта.
то, что вы обычно называют ORM - это пакет готовых реализаций паттернов Repository/Data Mapper/UoW/Lazy Load/Identity Map. Зачем задавать вопрос "зачем он нужен" если вы каждый день пользуетесь репозиторием, встроенным в ваш ORM?
@Pasha
ОтветитьУдалитьЭто я к тому, что можно передавать ILinqProvider прямо в Query минуя Repository.
@Pasha
ОтветитьУдалить>судя по названию - этот свой IQueryable. Чем он лучше стандартного?
Он инкапсулирует технологию доступа к данным за контролируемые границы. Как используя IQueryable запросить по данным, которых нет в доменной модели, но есть в модели отображения данных на сущности, или эти данные приватны в доменной модели?
Должна быть граница, выставление IQueryable наружу усложняет проведение(контроль) границы, но при этом облегчает разработку, не спорю, особенно если его потребителем является тот же разработчик, который разрабатывал схему маппинга.
@Nikita Govorov, а так ли нужно инкапсулировать технологию доступа к данным? Не в идеальном мире, а в реальности.
ОтветитьУдалить@Idsa
ОтветитьУдалитьВ команде больше одного человека, однозначно да.
Александр, а зачем?
ОтветитьУдалить@Nikita Govorov
ОтветитьУдалитьЕстественно, IQueryable нельзя выставлять наверх из BL/доменной модели. Это интерфейс для выборки данных, между ORM и BL.
Наверх, в представление, уходят строго списки. Представление ничего не знает о репозитории (или обертке).
Не совсем представляю себе ситуацию "нет данных в доменной модели" - т.е. она не соотвествует предметной области? Можно пример?
Данные приватны в доменной модели - ок, не вижу проблем. Код построения expression/спецификаций тоже находится строго в BL, и вполне видит приватные данные.
@Александр
>> Это я к тому, что можно передавать ILinqProvider прямо в Query минуя Repository.
Если учесть, что ILinqProvider - это реализация репозитория в ORM, а репозиторий - это просто обертка, то ваше предложение читается как "можно передавать репозиторий прямо в Query минуя обертку". Да, можно.
@Idsa layer skipping нехорошая вещь без острой необходимости. но технология доступа данным и так обычно инкапсулирована в ORM. Попробуй выполнить чистый SQL в L2S, имея только IQueryable :)
@Idsa
ОтветитьУдалитьКак используя IQueryable запросить по данным, которых нет в доменной модели, но есть в модели отображения данных на сущности, или эти данные приватны в доменной модели?
@Pasha
ОтветитьУдалить>Естественно, IQueryable нельзя...
Как я понимаю вы выставляете IQueryable в операционный слой? Что такое BL?
>Не совсем представляю...
http://ayende.com/blog/4054/nhibernate-query-only-properties
>Данные приватны в доменной модели - ок...
Приведите, пожалуйста, пример, я перестал понимать вашу точку зрения.
@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 > ....)
Точно так же будет выглядеть в варианте Александра, только код чуть в другом месте будет написан. В чем проблема?
>> Приведите, пожалуйста, пример, я перестал понимать вашу точку зрения.
Тогда лучше вы уточняйте, что за приватные данные. Не имеющие маппинга в базу?
@Pasha
ОтветитьУдалитьПро приватные данные в доменной модели, первый пример приходящий в голову, есть User у него есть скрытое(private) поле пароль, как через IQueryable написать запрос производящий поиск по имени пользователя и паролю?
@Nikita поле приватное, в базе не хранится, и маппинга для него нет? тогда в общем случае - никак - пользователя нельзя найти в базе по паролю.
ОтветитьУдалитьВ частном - тот же L2S позволяет создать фейковое public свойство Password, замапить его на колонку Password в базе, и в маппинге указать storage-ем скрытое поле. Фильтрация и сортировка по паролю работать будет при выборке через LINQ.
Но непосредственное обращение к свойству с целью прочитать или записать будет бросать исключение/делать хэширование при записи.
Другие провайдеры тоже такие финты позволяют, скорее всего.
Как я уже говорил, лично я считаю данный подход лишь очередной разновидностью реализации паттерна Specification, который в данном случае еще и инкапсулирует сам запрос, помимо предиката.
ОтветитьУдалитьЧем это лучше GenericRepository + Specification, в общем-то не ясно. Те же проблемы )
Касательно CQRS так часто тут упоминаемого, то не уверен что оно именно CQRS, а не CQS. Именно это видно из примеров.
> Самое главное, что мы уходим от не тестируемых репозиториев к маленьким тестируемым классам с единственной отвественностью и понятной логикой.
ОтветитьУдалитьимхо в данном случае вы производите лишь DAL, который, к тому же, судя из комментов, требует IoC и прочей ерунды, что усложняет саму сущность.
Repository же в основе своей очень прост.
Скорее всего проблемы с ним возникают из-за недопонимания его предназначения.
Ведь корни его лежат в Java, где никаких IQueriable не было. А теперь люди пытаются мешать совершенно разные парадигмы.
>>А теперь люди пытаются мешать совершенно разные парадигмы.
ОтветитьУдалитьВы так говорите, как будто это плохо :)
@Pasha
ОтветитьУдалитьПоле приватное, в базе хранится, и маппинг для него есть.
> Вы так говорите, как будто это плохо :)
ОтветитьУдалитьЭто плохо когда отсутствует понимание того, для каких целей это создавалось.
Начнём с того, что изначально самое плохое - это попытка применить Repository в обычном веб-приложении. Где кроме данных и DTO в принципе ничего не нужно)
Отсюда и выплывает куча топиков и проблем :-)
Подводя итоги сегодняшнего вечера, можно сделать вывод, что, вместо одного GenericRepository и 20 Specification мы имеем 25 QueryObject'ов.
ОтветитьУдалитьОсталось понять чем это помогает)
Понимаю чем удобно, а так же опасно, протаскивать IQueriable. Но в данном случае мы боремся с ветряными мельницами.
@Nikita
ОтветитьУдалить>> Поле приватное, в базе хранится, и маппинг для него есть.
раз ваш провайдер поддерживает маппинг приватных полей (сомнительная фича, если честно), то он должен поддерживать указание приватных полей как storage для паблик свойств. Т.е. все как я выше расписал - фейковое свойство, и проблема решена.
Хотя сама идея изменять приватные поля извне класса, да еще и неявно - это дичайшее нарушение инкапсуляции, после которого вообще нельзя доверять модификаторам доступа во всем проекте. По сравнению с ним - протаскивание IQueryable куда угодно - детские шалости :)
@Pasha
ОтветитьУдалитьORM должен уметь отображать на любые поля(свойства). В модели мое поле явно приватное(ну или protected), и я не хочу давать к нему доступ из вне. Соответственно я не могу использовать IQueryable для запроса по таким данным. Заведение фейковых полей и прочие костыли в модели не интересно в принципе.
Материализация объектов ORM'м, так же как допустим и десериализация, это низкоуровневые процессы, задача которых, восстановить внутреннее состояние объекта.
Вы так не считаете? У вас все свойства сущностей имеют public prop {get;set}?
Вы можете использовать IQueryable для запроса по таким данным, без костылей. Внутри класса User вы сможете отфильтровать IQueryable по паролю.
ОтветитьУдалитьИзвне, теоретически - нет, потому что вне класса User о существовании поля ничего не известно. Нельзя фильтровать по том, чего нет.
На практике, если очень хочется - то можно кодом собрать вообще любой Expression>, и передать его в Where. Снаружи будет тот же фильтр .WithPassword(pass). Внутри - будет код, в котором будет упоминание свойства по имени, типа "password". По концентрации анти-ООД - это то же, что задание фильтра как "select users from user where password = ?".
Этот блог вроде как посвящен ООП/ООД, и за упоминание свойства по имени я рискую получить пожизненный бан.
Да, у нас все поля сущностей { get; set; }. Десериализация, IMO, внешний для сущности процесс. Кто-то не доверяет своим разработчикам, кто-то - бинарными сериализаторам.
@Pasha
ОтветитьУдалить>Вы можете использовать IQueryable для запроса по таким данным, без костылей.
>Внутри класса User вы сможете отфильтровать IQueryable по паролю.
Действительно, это вариант. Вот только странно со стороны будет смотреться, да и ваши любимые ext. meth. нельзя во вложеннных классах объявлять.
>Извне, теоретически - нет, потому что вне класса User о существовании поля ничего не известно. Нельзя фильтровать по том, чего нет.
>По концентрации анти-ООД ...
>Этот блог вроде как посвящен ООП/ООД, и за упоминани...
О существовании поля известно в слое доступа к данным. И этим нужно там пользоваться, magic strings, HQL, SQL все равно. Смешение парадигм неизбежно, не стоит этому противиться,
лишь бы была изоляция.
>Кто-то не доверяет своим разработчикам...
Вопрос не в доверии к разработчикам, вопрос в возникновении нередко необходимости инкапсуляции внутреннего состояния объектами модели,
поэтому IQueryable по своей природе не в состоянии помочь в таких случаях.
@Nikita ну ок, пусть будет смешение парадигм, не ООП, ext. meth с упоминанием приватного поля по имени. зато изоляция. It Depends.
ОтветитьУдалить@Gengzu
ОтветитьУдалить> имхо в данном случае вы производите лишь DAL, который, к тому же, судя из комментов, требует IoC и прочей ерунды, что усложняет саму сущность.
Зависит от реализации, обязательное использование IoC не нужно. Ну и конечно Query ничего не знает про IoC, в это суть DI.
> Repository же в основе своей очень прост.
Скорее всего проблемы с ним возникают из-за недопонимания его предназначения.
Я думаю, что проблемы с ним возникают, когда в репозитории становится слишком много методов и его ответственность разрастается. Тоже самое происходит с сервисами, которые надо разбивать на команды.
> Ведь корни его лежат в Java, где никаких IQueriable не было. А теперь люди пытаются мешать совершенно разные парадигмы.
Да, в этом что-то есть. IQueryable очень сильно раздвинул границы.
@Gengzu
ОтветитьУдалить> Подводя итоги сегодняшнего вечера, можно сделать вывод, что, вместо одного GenericRepository и 20 Specification мы имеем 25 QueryObject'ов. Осталось понять чем это помогает)
Во-первых, уже отсюда понятно, что репозиторий не нужен. Во-вторых, если у вас действительно только спецификации и репозиторий с одним публичным методом, то всё хорошо. Другой вопрос, что делать с репозиториями в "обычном" их понимании. @Idsa уже приводил пример такой реализации http://pastebin.com/guwfbrgv
Может для вас это будет шок, но 99% репозиториев выглядят именно так. Поэтому решением является разбить такой singltone-repository на небольшие объекты.
> Понимаю чем удобно, а так же опасно, протаскивать IQueriable. Но в данном случае мы боремся с ветряными мельницами.
Я думаю, что удобство/опасность балансируют в зависимости от: длительности проекта, необходимости внесения изменений, текучки в команде, уровня команды и др. факторов. Что скажете?
Александр, не подскажешь как связываются UnitOfWorkFactory и QueryFactory в AccountController. Не совсем ясно, как сущности выдаваемые из Find*Query будут отслеживаться UoF для последующего Commit(). По предлагаемому коде не видно как ILinqProvider попадает в QueryFactory.
ОтветитьУдалитьНет ли где-то этого кода в твоем публичном SVN репозитории, чтобы посмотреть?
Спасибо!
@ALive
ОтветитьУдалитьПримерный код есть здесь http://code.google.com/p/nhibernate2-unitofwork
Если вкратце, то в случае с NH эти два объекта оборачивают объект Session.
> Я думаю, что проблемы с ним возникают, когда в репозитории становится слишком много методов и его ответственность разрастается. Тоже самое происходит с сервисами, которые надо разбивать на команды.
ОтветитьУдалитьСлишком много методов в нём возникает как раз из-за недопонимания его предназначения.
Я уже пытался акцентировать внимание, что
GenericRepository + Specification имеет лишь 5 методов. больше не нужно.
по функциональности получается равноценный вариант с IQuery, где спецификации еще и можно наследовать.
> Другой вопрос, что делать с репозиториями в "обычном" их понимании. @Idsa уже приводил пример такой реализации http://pastebin.com/guwfbrgv
> Может для вас это будет шок, но 99% репозиториев выглядят именно так. Поэтому решением является разбить такой singltone-repository на небольшие объекты.
Не нужно делать такие репозитории. А главное, не нужно пихать их туда, где они не нужны. Веб-сайты тому пример.
> Я думаю, что удобство/опасность балансируют в зависимости от: длительности проекта, необходимости внесения изменений, текучки в команде, уровня команды и др. факторов. Что скажете?
Само собой. в комманде из 1го человека, на проекте в 1 человеко месяц имхо допустимо использование IQueriable прямо в контроллере)
@Gengzu
ОтветитьУдалитьМы делаем системы по автоматизации бизнес-процессов через web-интерфейс на ASP.NET MVC. Длительность последнего проекта 10 месяцев командой разработчиков.
Пусть это даже веб-проект, но о проектировании подумать надо. Вряд ли выбор думать о дизайне системы или нет зависит от технологии.
веб-интерфейс к бизнесс приложению и веб-сайт - разные вещи)
ОтветитьУдалитьи всё же, чем вышеописанный подход лучше репозитория со спецификациями?
@Gengzu
ОтветитьУдалитьПри правильной реализации спецификаций + репозиторий только тем, что нет репозитория.
Еще этот подход на много легче реализовать, чем специфицации.
Представьте себе человека, который не слышал ни про репозиторий, ни про Query.
Он наверняка быстрее освоит создание маленьких объектов, чем создание спецификаций, которые надо передавать в репозиторий.
Возможно. Спорить не стану.
ОтветитьУдалитьКак по мне, так спецификация более атомарна, что ли.
В отличии от Query, которые одновременно и содержит данные и использует их, в одном месте.
Да и использовать мне кажется это менее удобно.
Плюс спецификации можно использовать для валидации бизнесс-сущностей, что даёт им дополнительное преимущество :-)
Для начала - спасибо за пост и за развившуюся любопытную дискуссию :)
ОтветитьУдалить> но 99% репозиториев выглядят именно так. Поэтому решением является разбить такой singltone-repository на небольшие объекты.
Александр, так мы говорим о рефакторинге существующих проектов, или об архитектуре, как она должна выглядеть при проектировании с нуля?
В примерах по @Idsa репозитарий остается, но они не "по одному на каждую сущность" - а один абстрактный.
Трудоемкость на добавление запроса в случае спецификаций имхо ниже.
Вливание в проект новых участников не выше - спецификация еще более стандартный шаблон, чем CQRS, если спецификации на linq - то проблем вообще нет. Объяснить человеку (на существующих примерах), что спеки надо передавать в репозиторий - это не большая проблема, чем обосновать, почему на каждый запрос нужен отдельный класс. В любом случае объяснение - вопрос 15 минут, время привыкания - одинаково.
При условии "внешних"(IQueryable) наложений инклюдов-сортировок-лимитов число спецификаций очевидно ниже, чем классов в CQS.
В чем оставшиеся преимущества CQS?
Dependency-injection? Так мы получаем query строго заточенные под единственное использование - в том месте, куда он инъектируется. А всё-таки основной плюс инверсии зависимостей в выделении сервисов-операций, которые хотя бы теоретически могут быть переиспользуемы.
На CQS проще накладывается cross-cutting логика типа кэширования. Но тем самым мы только усугубляем заточенность под единственное использование - если в некоторых вью допустимо кэширование, то при некоторой бизнес-логике может быть просто необходимо использовать актуальные данные.
> При правильной реализации спецификаций + репозиторий только тем, что нет репозитория.
С учетом этой фразы моя аргументация несколько теряет смысл, да :) Могу только извиниться за поздное вливание в обсуждение :)
@Shaddix, отличное summary!
ОтветитьУдалитьСправедливости ради, отмечу, что приведенный репозиторий не глобальный - это репозиторий для тэгов (все запросы там выполняются относительно ctx.BaseTags). А ItemTag и др. - наследники BaseTag.
В качестве подтверждения привожу ссылку на папку Repositories в svn, где можно найти еще несколько десятков подобных репозиториев: http://code.google.com/p/thingface/source/browse/#svn%2Ftrunk%2FData%2FRepositories
Попытка номер два. Ссылка
ОтветитьУдалить@Shaddix
ОтветитьУдалить> Александр, так мы говорим о рефакторинге существующих проектов, или об архитектуре, как она должна выглядеть при проектировании с нуля?
И первое, и второе.
> В чем оставшиеся преимущества CQS?
Dependency-injection? Так мы получаем query строго заточенные под единственное использование - в том месте, куда он инъектируется.
Сами Query никуда не инжектируются, инжектируется только IQueryFactory. Остальной проект не знает ни про какие *Query, это в отличие от спецификаций и репозитория.
> если в некоторых вью допустимо кэширование, то при некоторой бизнес-логике может быть просто необходимо использовать актуальные данные.
В чем проблема?
> С учетом этой фразы моя аргументация несколько теряет смысл, да :) Могу только извиниться за поздное вливание в обсуждение :)
Продолжаем обсуждение :) Истина где-то рядом.
@Idsa
ОтветитьУдалитьСпасибо за ссылки! Узнаю наши репозитории, такими они были, пока их не покрамсали на Query :)
Здесь уже приводили код для Query, для классических репозиториев (будь они неладны), но не было кода для IQueryable-based репозиториев.
ОтветитьУдалитьВот ссылочка на проект с репозиторием и спецификациями, который я начал разрабатывать уже после того, как перешел на IQueryable: ссылка
@Idsa
ОтветитьУдалитьСпасибо за примеры, вы привели их уже довольно много.
У меня есть несколько вопросов/замечаний по поводу этой реализации.
В объекте Repository меня смущает метод GetAll (зачем он нужен?), и методы UnitOfWork - Add и SaveChanges.
И еще не понятно, почему спецификации - это статические методы? Могут ли спецификации накладываться друг на друга?
@Александр
ОтветитьУдалитьХорошие вопросы.
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 перегружает операции и позволяет накладывать спецификации друг на друга.
Почему статические? Хм... Статические удобнее использовать, и при этом я пока не заметил недостатков у этого подхода. По крайней мере статические спецификации никак не ограничивают тестируемость репозиториев (могу привести пример, подтверждающий эту мысль).
> А вот об Add я бы этого не сказал. Мне кажется, ему самое место в репозитории. Или нет?
ОтветитьУдалитьМне как-то больше нравится вот такой подход P of EAA Catalog | UnitOfWork
Хм... На текущий момент мое понимание взаимоотношений между UnitOfWork и репозиториями заключается в том, что репозиторий управляет entity-specific операциями (Add, Delete), а UnitOfWork управляет операциями по контексту, сессии в целом (SaveChanges, ClearChanges).
ОтветитьУдалитьХотя, с другой стороны, Add и Delete модифицируют UnitOfWork. И если бы не было прослойки в виде EF контекста, такие методы пришлось бы явно вызывать через UnitOfWork.
Я в сметении :)
Мысли вслух. То, что нынче именуют UnitOfWork в сравнении с фаулеровским UnitOfWork, скорее, нужно называть UnitOfWorkManager, потому что настоящий UnitOfWork встроен в ORM.
ОтветитьУдалитьЕсли по существу, то мне не нравится, что для того чтобы выполнить запрос
ОтветитьУдалить.Where(x => x.Email == email) .SingleOrDefault();
мне требуется написать:
1. целый класс FindAccountByEmailQuery
2. метод в интерфейс IQueryFactory
3. метод в класс QueryFactory
и протащить параметр email через все эти "слои".
Разве не очевидно дублирование кода и что при любом изменении придется править в трех местах?
@Nikolay Sergeev
ОтветитьУдалитьПотрудитесь прочитать комментарии выше.
QueryFactory - это одна из возможных реализаций идеи. Можно делать тоже самое вообще не реализуя IQueryFactory.
Идея в том, что надо избавиться он больших объектов Repository, которые становятся фактически Singletone'ми.
Александр, очень хорошая статья, спасибо. Я сейчас как раз столкнулся со стремительным разрастанием репозиториев в проекте. Мало того, у нескольких (не всех) репозиториев может присутствовать общая логика. В этом случае наследование применять как-то совсем не хочется. Рефакторинг инфраструктуры сейчас мне представляется как раз в выделении методов репозиториев в отдельные Query объекты.
ОтветитьУдалитьНе очень понятным осталось только одно: как вы предлагаете реализовывать запросы на сохранение/модификацию domain-сущностей?
@Mr Fontaine
ОтветитьУдалитьСохранением и модификацией занимается UnitOfWork, который автоматически отслеживает состояние объектов.
Можете посмотреть для примера http://blog.byndyu.ru/2010/07/2-unit-of-work_10.html
Александр, конкретно в моем случае приходится работать с Ado.net (реализация конкретных репозиториев построено на Ado.net). То есть я только лишь собственными силами создаю некое подобие ОРМ. Использовать DataContext из LinqToSql или Session из NHypernate я не могу (таковы условия задачи). Реализация же самостоятельно с нуля IUnitOfWork представляется мне довольно таки трудоемкой задачей. Интересно было бы услышать ваше мнение на этот счет.
ОтветитьУдалить@Mr Fontaine
ОтветитьУдалитьВозможно вам больше подойдет шаблон ActiveRecord. Могу посоветовать полистать книжку, возможно найдете решение близкой к своей проблеме http://martinfowler.com/books.html#eaa
Есть вариант посмотреть исходники NHibernate и сделать свой велосипед ля ADO.NET.
Ну и наконец перейти на нормальную ORM со временем.
сделал статический репозиторий, потом два дня пытался выловить баг - данные кэшировались
ОтветитьУдалитьБлин я студент фита, работаю, сейчас в отпуске, перечитал весь ваш блог. столько нового и полезного за все время обучения в вузе не видел. Выйду с отпуска с таким багажом знаний коллеги не узнают меня) Спасибо вам.
ОтветитьУдалитьСпасибо за поддержку.
ОтветитьУдалитьВозможно я не все понял. Ну не получается ли у нас при таком подходе
ОтветитьУдалитьQueryFactory становится God Объектом? И не получатеся ли у нас что "ФабрикаЗапростов" это почти тот же самый Репозиторий только с использование шаблона Specification( ILinqProvider)?
p.s. Я не в коем случаи не придираюсь. Просто хочу разобраться, т.к. остаються непонятки, а опыта мало.
Возможно я не все понял. Ну не получается ли у нас при таком подходе QueryFactory становится God Объектом? И не получатеся ли у нас что "ФабрикаЗапростов" это почти тот же самый Репозиторий только с использование шаблона Specification( ILinqProvider)?
ОтветитьУдалитьp.s. Я не в коем случаи не придираюсь. Просто хочу разобраться, т.к. остаються непонятки, а опыта мало.
Артем, 2 момента:
ОтветитьУдалить1. QueryFactory не содержит кода, поэтому по определению не может быть God Object. В ней нет никакой логики
2. Эта фабрика приведена для примера, посмотрите во что она может превратиться http://blog.byndyu.ru/2011/08/queryfactory-iqueryfactory.html
Добрый день !
ОтветитьУдалитьУ меня вопрос про то, как тестировать ваш класс ...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.
Подскажите, что вы ожидаете от теста? Вы хотите понять правильно ли работает лямбда-выражение?
ОтветитьУдалитьто, что при наличии в репозитории только одного объекта метод Excecute вернет набор объектов только с одним элементом.
ОтветитьУдалитьхотя бы так.
можно усилить - при наличии в репозитории объекта со значением поля 12 возвращается объект с таким же значением поля.
но Moq объект для репозитория не срабатывает, когда он передается в качестве реализации интерфейса в класс Query
Подскажите, у вас есть интерфейс IBaseRepositoryUnit, что за метод FindListQ?
ОтветитьУдалитьпо заданному Expression возвращает набор объектов
ОтветитьУдалитьреализация может быть такой
public virtual IQueryable FindListQ(Expression> predicate)
{
return Query(predicate);
}
вызываться может так
var list = repository.FindListQ(t => t.OperGroupId == 12);
В этом случае проще всего использовать Stub. Вот пример http://pastebin.com/rVwx4VJN
ОтветитьУдалитьспасибо большое - я к таким же выводам пришел.
ОтветитьУдалитьПочему Вы называете QueryFactory фабрикой, если он не возвращает объектов query? В данном случае это скорее фасад с инжекцией зависимости, т.к. он возвращает результат выполнения query а не сам query...
ОтветитьУдалитьprikolno kruto spasibo
ОтветитьУдалитьДовольно интересная статья, а ещё бы побольше статей на тему DOM & JQUERY или DOM & JAVASCRIPT. Хочу написать свой сайт и парсить туда всякую информацию. В интернете уже столько доменов появилось что не знаешь какое имя зарегистрироваться. Недавно пробовал открыть что нибудь связанное с едой и взял имя с одной дорамы Хупэпт ну или repept в результате выяснилось что домен то http://repept.ru уже занят и пришлось сделать какой то kolbasotes. А вообще подскажите сложно ли написать свой грабер статей или нет ? Хочу с этом сайта всё собрать что бы ему не повадно было, а то обидно.
ОтветитьУдалить