Как стать автором
Обновить

Комментарии 25

Осталось развить Criteria API и получится NHibernate )
Я хотел сказать, что код вида
var account = Query.For().With(new LoginCriterion(login));

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Публикации