Pull to refresh

Comments 161

Для меня репозиторий, прежде всего, элемент Business Layer. Что он зависит (если зависит) от Data Layer — деталь его реализации, скрытая от других элементов Business Layer и всего Presentation Layer. А DAO (если он используется) — как раз элемент Data Layer с которым работает репозиторий.

Разное понимание, по-моему, возникает прежде всего из-за универсальных конфигурируемых ORM. Их универсальность является основной причиной того, что через интерфейсы репозитория протекает Data Layer

Для меня паттерн "репозиторий" выполняет две функции:


  • Отделяет код работы с внешним состоянием (хранилищем, кэшем) от бизнес-логики (я предпочитаю SOA и anemic model), позволяя писать тесты с моками.
  • Инкапсулирует код работы с данными, который потенциально может изменить место выполнения: например, был LINQ-запрос, стала table function, было ORM-сохранение, стала хранимая процедура.
Проблем с тестированием этого не возникнет? В одной сущности намешано несколько вещей (SPR не соблюдается), как я вас понимаю.

Проблем с тестированием чего?
С тестированием бизнес логики нет проблем — код работы с состоянием вынесен в репозиторий, получаются практически stateless сервисы.
С тестированием репозитория — обычно тесты на репозиторий не пишу, т.к. код типовой и использует ORM.

Если у вас в репозитории только linq запрос или вызов хранимки, разве это репозиторий? Это «тупой» DAO.
Репозиторий — это не начинка, а функция для клиентов. Они обращаются к нему на языке домена, а не физического хранилища.
Если у вас в репозитории только linq запрос или вызов хранимки, разве это репозиторий?

Конечно, репозиторий. До тех пор, пока вы снаружи предоставляете соответствующий интерфейс (а Expression, как ни странно, вполне удовлетворяет паттерну Query Object), совершенно не важно, что внутри.

Мне необходимо понять о чем именно вы говорите.
Вы считаете CRUD-интерфейс — это нормально для репозитория? Я о таком, где, например:
IList Load()
void Update(T item)
void Add(T item)
void Delete(T item)

Или в репозитории должны быть методы типа, но не CRUD:
IList GetItemsBy(int id)
IList GetItemByParent(TParent p)

?

Во-первых, ваши "но не CRUD" методы — это всего лишь специализация R из CRUD.
Во-вторых, вот что считает Эванс:


For each type of object that needs global access, create an object that can provide the illusion of an in-memory collection of all objects of that type. Set up access through a well-known global interface.

Provide methods to add and remove objects, which will encapsulate the actual insertion or removal of data in the data store. Provide methods that select objects based on some criteria and return fully instantiated objects or collections of objects whose attribute values
meet the criteria, thereby encapsulating the actual storage and query technology.

Таким образом, типичный CRUD-интерфейс


interface IRepository<T,TKey>
{
  T Get(TKey key);
  IQueryable<T> Query();
  Add(T entity);
  Delete(T entity);
}

вполне является репозиторием по Эвансу (и по Фаулеру, поскольку Фаулер ссылается на Эванса).

> IQueryable Query();

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

Где у вас «methods that select objects based on some criteria»? Где «encapsulating the actual storage and query technology»?
Никогда не наступали, что ORM не может выполнить запрос?

Наступал, конечно.


Где у вас «methods that select objects based on some criteria»?

Вот:


  T Get(TKey key);
  IQueryable<T> Query();

Где «encapsulating the actual storage and query technology»?

А где вы видите, чтобы хранилище или механизм запросов были выставлены наружу?

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

То есть я утверждаю, что разница между
IQueryable Query();
IEnumerable Query(string whereSqlExpression);

только в синтаксическом сахаре.

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

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


IQueryable Query();
IEnumerable Query(string whereSqlExpression);

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

Разница в том, что метод FindAllActiveCustomers даёт привязку прикладной модели, и не решето.
Разница в том, что метод FindAllActiveCustomers даёт привязку прикладной модели, и не решето.

Угу. А потом вам надо найти всех активных кастомеров с вип-статусом, созданных за последнюю неделю. Что будем делать?


(ну и да, FindAllActiveCustomers — не спецификация)

писать заведомо выполнимый linq в БЛ, например.
в бизнес-логике

FindAllActiveCustomers().Where(...)

Кстати, а как вы тестируете, что ваш неработающий запрос от IQueriable работает?
FindAllActiveCustomers().Where(...)

А что возвращает FindAllActiveCustomers?


Кстати, а как вы тестируете, что ваш неработающий запрос от IQueriable работает?

Интеграционными тестами.

fully instantiated collections of objects whose attribute values meet the criteria

IEnumerable Custumer

> Интеграционными тестами.

Тоже метод.
fully instantiated collections of objects whose attribute values meet the criteria

Если буквоедствовать, то IEnumerable — это не fully instantiated collection, там же lazy.


IEnumerable Custumer

Ага. Полтора миллиона активных кастомеров — перебирать в памяти сервера под Where. Круть.


Если серьезно, то все это опять компромисы.


На одном конце спектра — явные intention-revealing методы навроде GetActiveCustomers, GetPendingBills, и все такое. Красиво, явно, читабельно, внутри максимально заоптимизировано, ничего никуда не течет. Но — количество таких методов растет как комбинация всех вариантов поиска/фильтрации (а у нас еще есть сортировки, группировки и агрегаты), а если какого-то метода недодали, то придется использовать локальную обработку на сервере со всеми вытекающими. Мой любимый пример — это требование построить поверх такого репозитория типичный для LOB-приложения грид на 18 колонок с сортировкой/фильтрацией по любой из них (включая любую их комбинацию по and/or).


На другом конце спектра — открытый IQueryable. Максимальная гибкость, поддержка по инфраструктуре (есть и готове UI-компоненты, и готовые сервисные адаптеры), теоретическая возможность сделать оптимизацию по производительности для любого специфического кейса. Но — абстракция неизбежно течет, необходимо помнить возможности/недостатки конкретного query provider, каким бы хорошим он ни был, слишком легко написать ad-hoc вместо использования intention-revealing (заметим, она тем не менее возможна: repository.Query().AllActive().UpdatedSince(...)).


Где-то посередине — спецификации. The best and the worst of two worlds. Intention-revealing, компонуются, внутри можно написать что угодно, включая оптимизацию по производительности. Но — поддержки обычно нет ни в UI, ни в провайдерах, всю адаптацию надо писать самому, все надо придумывать самому (например, как сделать, чтобая была честная поддержка времени компиляции). В итоге, проектов, в которых эти вложения оправдаются, не так уж и много.

> Если буквоедствовать, то IEnumerable — это не fully instantiated collection, там же lazy.

Если буквоедствовать, то IEnumerable — интерфейс. Причём «IEnumerable is the base interface for all non-generic collections that can be enumerated»

> Ага. Полтора миллиона активных кастомеров — перебирать в памяти сервера под Where. Круть.

Ну дёрнете ToList от своего IQueriable — получите 6 миллионов неактивных кастомеров. Мы репозиторий обсуждаем или как в памяти полтора миллиона объектов покрутить?

> а если какого-то метода недодали, то

Ну, доделайте.

> Мой любимый пример — это требование построить поверх такого репозитория типичный для LOB-приложения грид на 18 колонок с сортировкой/фильтрацией по любой из них (включая любую их комбинацию по and/or).

Спрошу страшное: зачем требовать делать это поверх репозитория?

Если серьёзно, то это задача оптимизации. Оптимизация требует костылей. Добавить в репу IQueryable ради грида можно, но _ради грида_. На это не будет ни одного теста (я не видел людей, желающих писать тесты на сортировку грида). И это не повод этот костыль узаканивать.

В своём коде я текущих абстракций видеть не хочу. Дать инфраструктуре IQueryable — на здоровье, UI-фреймворки вообще за рамками DDD.

Есть ещё грустная тема «IQueryable и попытка показать на UI кастомный DTO», где вся «гибкость» IQueryable превращается в поле с граблями.
Ну дёрнете ToList от своего IQueriable

Так зачем же его дергать-то?


Ну, доделайте.

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


Спрошу страшное: зачем требовать делать это поверх репозитория?

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


Если серьёзно, то это задача оптимизации. Оптимизация требует костылей

Я потому и говорю: мы балансируем компромисы. IQueryable — не самый худший из них.


В своём коде я текущих абстракций видеть не хочу.

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


UI-фреймворки вообще за рамками DDD.

Это сложный вопрос, неоднозначный. У нас есть куча сугубо бизнесовых задач, которые связаны с UI, и если мы половину из них поведем мимо DDD, у нас домен начнет рассыпаться.


Есть ещё грустная тема «IQueryable и попытка показать на UI кастомный DTO»,

Как ни странно, проекции обычно работают. А когда они не работают, все остальное не работает приблизительно так же, так что уже не важно.

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

Угу, когда вы напишите бесконечное приложение. Давайте не передергивать?

> А я не хочу видеть в своем коде комбинаторный взрыв псевдо-бизнесовых методов на репозитории.

Аргумент звучит хорошо. Но на практике комбинаторного взрыва не будет.
Именно потому, что методы бизнесовые, а не кодогенерацией.

> А если мы где-то отказываемся от репозиториев, то внезапно выясняется, что и дальше от них тоже неплохо можно отказаться.

Ну, если замена unit-тестов на интеграционные — «неплохо»… Да и до Эванса с фаулером программы писали.
А комбинаторного взрыва интеграционных тестов не боитесь?

> Я потому и говорю: мы балансируем компромисы.

Использовать IQueriable для UI — это практическая необходимость. Но она не диктует дизайн остального, она не должна учитываться в домене. Иначе «у нас домен начнет рассыпаться.».

> Это сложный вопрос, неоднозначный. У нас есть куча сугубо бизнесовых задач, которые связаны с UI, и если мы половину из них поведем мимо DDD, у нас домен начнет рассыпаться.

UI и UI-фреймворк — разные вещи.

> Как ни странно, проекции обычно работают. А когда они не работают, все остальное не работает приблизительно так же, так что уже не важно.

Если на SQL транслируется — работает. Может даже с фильтрацией. Может даже с фильтрацией без full table scans.
А программист в здравом уме не напишет такого(тем более без ленивого IQueryable в руках), что пользователь на гриде нащелкает. Так что нет, остальное вполне может работать.
Аргумент звучит хорошо. Но на практике комбинаторного взрыва не будет. Именно потому, что методы бизнесовые, а не кодогенерацией.

Это зависит от количества разработчиков на конкретном бизнес-секторе.


Ну, если замена unit-тестов на интеграционные — «неплохо»…

А кто что-то сказал про замену?


А комбинаторного взрыва интеграционных тестов не боитесь?

Нет, не боюсь. Машина железная, пусть считает.


Использовать IQueriable для UI — это практическая необходимость. Но она не диктует дизайн остального, она не должна учитываться в домене. Иначе «у нас домен начнет рассыпаться.».

Вы предлагаете не учитывать UI в домене? Это смело.


Если на SQL транслируется — работает.

Я согласен, когда проекции транслируются на SQL, они работают. Только они не всегда транслируются.


А программист в здравом уме не напишет такого(тем более без ленивого IQueryable в руках)

Зато он может написать вложенный цикл, дающий степенную сложность. Не надо недооценивать программистов.

> Это зависит от количества разработчиков на конкретном бизнес-секторе.

Как у вас code review работает?

> Вы предлагаете не учитывать UI в домене? Это смело.

Нет, вы передергиваете смело.

> А кто что-то сказал про замену?

Вам для тестирования выполнимости запросов нужен интеграционный тест, мне для FindAllActiveCustomers().Where(...) — unit.

Звучит как замена.

> Зато он может написать вложенный цикл, дающий степенную сложность.

по IQueryable и без него — ооочень разный результат.
Как у вас code review работает?

Медленно.


Вам для тестирования выполнимости запросов нужен интеграционный тест, мне для FindAllActiveCustomers().Where(...) — unit.

А как вы тестируете, что FindAllActiveCustomers действительно возвращает то, что обещал?


Звучит как замена.

Домен можно продолжать тестировать юнит-тестами.


по IQueryable и без него — ооочень разный результат.

Your mileage may vary.

> А как вы тестируете, что FindAllActiveCustomers действительно возвращает то, что обещал?

Если там dc.Customers.Where(x => x.IsActive).ToList() — аналитически (никак).
Если что-то сложнее — можно dc.Customers замокать.
Если хранимка или ещё что — интеграционный.

> Домен можно продолжать тестировать юнит-тестами.

Можно, но где кверя отвалиться — не узнаешь.
Если там dc.Customers.Where(x => x.IsActive).ToList() — аналитически (никак).

Тогда можно и "никак" не тестировать внешние запросы к IQueryable.


Если что-то сложнее — можно dc.Customers замокать.

И как вам мок поможет от проблемы "тут написали запрос, который на самом деле не лезет в провайдер"?


Можно, но где кверя отвалиться — не узнаешь.

… и вот тут включаются интеграционные тесты. Да, покрытие меньше, но оно все равно есть.


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

> Тогда можно и «никак» не тестировать внешние запросы к IQueryable.

Если вы и ваши коллеги на глаз быстро и надежно оценивают запрос — можно.
За подобный надежно инкапсулированный запрос лично я уверен, за внешние — нет.

> И как вам мок поможет от проблемы «тут написали запрос, который на самом деле не лезет в провайдер»?

Что?
Что?

Ровно то, о чем написано выше. Вы же говорите, что проблема IQueryable в том, что к нему можно написать запрос, который не выполнится (да, можно, сам неоднократно влетал). Теперь представьте, что кто-то написал такой запрос внутри репозитория, реализуя очередную business query. Как это поймать тестами?

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

Если запрос не примитивный — перед «Как это поймать тестами?» стоит подумать «не стоит ли применить физическое воздействие за попытку утащить логику домена».

Ну и, раз у вас «Машина железная, пусть считает.» — напишите один тест, дергающий за все методы всех репозитариев на предмет отсутствия эксепшенов.
Если запрос примитивный — видно, что он выполниться. [...] Если запрос не примитивный — перед «Как это поймать тестами?» стоит подумать «не стоит ли применить физическое воздействие за попытку утащить логику домена».

О, а что же делать, когда нам надо (а) эффективно, т.е. быстро и (б) выполнить логику домена на данных?


Ну и, раз у вас «Машина железная, пусть считает.» — напишите один тест, дергающий за все методы всех репозитариев на предмет отсутствия эксепшенов.

Это хорошо, пока эти методы не принимают значений/не зависят от погоды на Марсе.

> О, а что же делать, когда нам надо (а) эффективно, т.е. быстро и (б) выполнить логику домена на данных?

Как с любой оптимизацией, запилить как-нибудь. Если это больше 1% случаев — что-то в консерватории не так.

> Это хорошо, пока эти методы не принимают значений/не зависят от погоды на Марсе.

Autofixture в помощь. Если от погоды на марсе появляются необработанные исключения — тоже что-то в консерватории не так.

Ну вот в большей части консерваторий что-то случается не так. К сожалению. Мир не идеален, да.

> что-то в консерватории не так.

> Ну вот в большей части консерваторий что-то СЛУЧАЕТСЯ не так.

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

Я не против IQueryable. Но это не «fully instantiated objects or collections of objects whose attribute values
meet the criteria, thereby encapsulating the actual storage and query technology». То есть не репозиторий по Эвансу.

Лично у меня дальше встаёт вопрос «зачем тогда репозитории, да и вообще DAL, который простоты разработки не добавляет?», а потом «DDD ли это?»

Дело тут не в формализме, это просто совсем другой подход.
Но это не «fully instantiated objects or collections of objects whose attribute values meet the criteria, thereby encapsulating the actual storage and query technology».

Да ну? Это интерфейс, ничем не хуже IEnumerable.


Лично у меня дальше встаёт вопрос «зачем тогда репозитории, да и вообще DAL, который простоты разработки не добавляет?», а потом «DDD ли это?»

Ну, без DAL вам обойтись (в современных условиях) не выйдет. А вот зачем нужны репозитории — это очень разумный вопрос.

> Да ну? Это интерфейс, ничем не хуже IEnumerable.

Ну, есть ещё хорошие интерфейсы, IDisposable, например. Они тоже не коллекции.

> Ну, без DAL вам обойтись (в современных условиях) не выйдет. А вот зачем нужны репозитории — это очень разумный вопрос.

Я про выделенный слой.
Я про выделенный слой.

Я тоже.

На одном конце спектра — явные intention-revealing методы навроде GetActiveCustomers, GetPendingBills, и все такое. Красиво, явно, читабельно, внутри максимально заоптимизировано, ничего никуда не течет. Но — количество таких методов растет как комбинация всех вариантов поиска/фильтрации (а у нас еще есть сортировки, группировки и агрегаты), а если какого-то метода недодали, то придется использовать локальную обработку на сервере со всеми вытекающими.

Наверное, лучше всего использовать эти методы и возвращать из них IQueryable<T>, но использовать его только для гридов или OData, а не для бизнес-логики.

Я на месте программиста бизнес-логики в таком случае буду чувствовать себя идиотом: мне вернули IQueryable, но использовать я его не могу. What?

Использовать-то его можно, но вот для чего?
Я вижу два применения:


  • ad-hoc обработка данных на стороне источника (базы) как в бизнес логике, так и для выборки данных пользователем. Это нормально, клиент не может получить доступ к неподходящим данному сценарию данным.
  • Применение бизнес-логики к "сырому" источнику данных — когда отдают полностью содержимое таблицы, и потребитель должен сам отфильтровать и выбрать данные, подходящие под сценарий, и так в каждом бизнес методе. А вот это уже плохо, хотя и позволяет, на первый взгляд, избежать "взрыва" количества методов репозитория. На самом деле, бизнес логика "на данных" никуда не денется, она все равно будет присутствовать, и в еще большем количестве.
Использовать-то его можно, но вот для чего?

Для реализации бизнес-требований, очевидно. Было у меня "выведите в отчет десять последних измененных документов", стало "выведите в отчет десять последних просмотренных документов".


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

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

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

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


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

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

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

Конечно, позволяет. Только вместе с этим он позволяет не получить данные, которые не нужны.


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

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


То есть это лучше, чем ручная фильтрация через Where в бизнес методах, но все еще хуже бизнес методов в репозитории.

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

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

Если этих методов много, то их будет много при любом способе реализации, будь то ad-hoc LINQ-запросы, методы расширения, или специализированные методы в репозитории. Вопрос в том, как удобнее сделать с точки зрения разработки и сопровождения.
На мой взгляд, методы в репозитории самые удобные — у них четкий контракт, их легко найти, они могут инкапсулировать сложную логику или разные способы доступа к данным.

Если этих методов много, то их будет много при любом способе реализации, будь то ad-hoc LINQ-запросы, методы расширения, или специализированные методы в репозитории.

А вот и нет. Проблема специализированных методов на репозитории в том, что они не поддаются композиции. Вот у вас был один метод — GetVipCustomers, и другой — GetCustomersWithRecentOrders. Теперь для какого-то бизнес-сценария вам надо выбрать всех vip-клиентов с недавними ордерами; и у вас есть ровно два способа это сделать: либо вы пишете метод GetVipCustomersWithRecentOrders, либо вы выбираете данные из обоих и делаете join. В случае с extension-методами вы можете написать Customers.OnlyVip().OnlyWithRecentOrders().

Мне ничего не остается, только согласиться с вами :) Спасибо за идею, выглядит прекрасно.


Но, все-таки есть сценарии, когда я бы использовал методы репозитория:


  • Фильтрация для текущего пользователя(хотя можно передавать user id)
  • Методы, потенциально могущие переехать в базу (для оптимизации)
  • Ну и, опять же, я предпочитаю не давать доступ к "сырой" (нефильтрованной) выборке из таблицы, чаще всего есть непересекающиеся наборы данных (только активные, только доступные для редактирования и т.д.). Это надежнее метода расширения, который можно забыть, и попортить данные.

Вы забываете, что IQueryable, в итоге, превращается в AST. Поэтому все, описанное вами, можно сделать и поверх IQueryable, просто на этапе его разбора внутри репозитория.

Если у вас extension-методы заточены под использование только с одним типом данных, то смысла в них столько же, сколько и в методах репозитория. В итоге получим одинаковое кол-во методов, только одни статик, а другие нет. Объясните еще раз разницу, пожалуйста, не уловил выше?

И второй момент: как в тестах замокать выборку не занимаясь настройкой данных? Например у вас проверяется алгоритм метода, который дергает такой вот .OnlyWithRecentOrders(), получая данные и что-то над ними делаеющий.
Если у вас extension-методы заточены под использование только с одним типом данных, то смысла в них столько же, сколько и в методах репозитория. В итоге получим одинаковое кол-во методов, только одни статик, а другие нет. Объясните еще раз разницу, пожалуйста, не уловил выше?

Во-первых, я могу делать generic extension-методы поверх интерфейсов (скажем, метод Active<T>(): where T: IActiveOrPassive). Во-вторых, я уже приводил классический пример композиции: у нас есть метод OnlyVIP и OnlyWithRecentOrders — теперь, чтобы получить випов с недавними заказами мы просто вызываем OnlyVIP().OnlyWithRecentOrders().


И второй момент: как в тестах замокать выборку не занимаясь настройкой данных?

Если вам хочется именно "не заниматься настройкой данных" — то пишете тестовый IQueryProvider, в котором разбираете AST. Но в среднем проще просто сгенерить in-memory dataset.

Подозреваю, что вопрос был о том, что бы не генерить тестовый датасет с данными, удовлетворяющими OnlyWithRecentOrders() и OnlyVIP(). Ибо это boilerplate.
Подозреваю, что вопрос был о том, что бы не генерить тестовый датасет с данными, удовлетворяющими OnlyWithRecentOrders() и OnlyVIP(). Ибо это boilerplate.

Тогда использовать свой IQueryProvider и разбирать AST.

Поэтому я и не люблю экстеншены, да.
C extension-методами понятен подход. Но все же мне кажется сильно универсального тут не сделать. В итоге все равно получится ворох методов аля шоткатов для блоков кода. Типа инлайн функций в C++ (если память мне не изменяет). Поэтому, не совсем понимаю реальное преимущество, хотя согласен, что удобно так в некоторых случаях.

А что такое AST?

Да, кстати, этот IQueryProvider, по-сути, и есть DAO. Не так ли?
Но все же мне кажется сильно универсального тут не сделать.

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


В итоге все равно получится ворох методов аля шоткатов для блоков кода.

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


А что такое AST?

Abstract syntax tree.

Мне кажется, дело тут не столько в универсальности.
Я, наверное, уже это писал где-то в комментах, но повторюсь.
У нас есть два, скажем так, вида бизнес логики "для данных".


  • Первый вид — это ограничения данных, доступных этому клиенту (здесь клиент и как пользователь, и как сценарий бизнес операции). Например, если у нас сервис для витрины веб-магазина, и репозиторий товара, то мы ни в каком виде не должны иметь доступа к неактивным товарам.
    Для этих случаев, я считаю, лучше иметь метод в репозитории, который точно не позволит забыть экстеншн метод.


  • Второй вид — это разнообразные методы для удобства. Это тоже бизнес логика, но она применяется ad-hoc. Это как бы сокращенные записи типовых выборок данных, для них методы расширения — отличное решение.
Для этих случаев, я считаю, лучше иметь метод в репозитории, который точно не позволит забыть экстеншн метод.

А как не забыть вызвать нужный метод репозитория?

Не иметь доступа к полной коллекции данных напрямую.

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


(другое дело, что в моей практике в итоге это условие всегда нарушалось)

А как вам такой подход, когда мы используем generic repository и specifications?
http://pastebin.com/sZYXHGv9

В моем опыте и личном мнении обобщенный репозиторий поверх хорошего ORM (или, в частности, поверх IQueryable) — избыточен. Никакой дополнительной пользы он не приносит, а вот ошибиться в нем можно больше одного раза.


В вашем примере как раз хорошо видно: много лишнего кода (включая собственный UoW и сортировку-по-именам-колонок), при этом все, что этот код делает — это трансляция из одного паттерна в другой. Можно выкинуть все промежуточные этапы между ProjectFilterDto и IQueryable, получив приблизительно такое:


    public DataTableList<ProjectItemDto> GetProjects(ProjectFilterDto filter)
    {
        if (filter == null)
            return null;

        return _dbContext
          .Projects
          .InStatus(filter.Status)
          .WithQueryOptions(filter)
          .ToDataTableList<ProjectItemDto>();
    }
Благодарю за ответ.

Получается вы предлагаете проблему сложных выборок решить extensions-классами. Я думаю, в этом случае мы можем получить большой God-объект по всем возможным выборкам, который будет по сути напоминать репозиторий с GetProjectInStatus и так далее.

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

По реализации выше: у меня всегда закрадывалась мысль о том, что код сам по себе избыточен и что надо как-то проще. Но данное вытекло именно из вашего подхода, так как сервисы были наводнены разными if в зависимости от того что выбрал пользователь на гриде и хотелось вынести это за рамки ответственности самого сервиса.
Получается вы предлагаете проблему сложных выборок решить extensions-классами

Extension-методами.


Я думаю, в этом случае мы можем получить большой God-объект по всем возможным выборкам

Не можем. Прелесть extension-методов именно в том, что они существуют сами по себе, отдельно от класса/интерфейса, которые они расширяют.


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

Для этого и существуют типовые конвертеры UI-запроса (обычно стандартного в рамках фреймворка) в expression tree. Когда ваш сценарий сводится к "взять сказанные пользователем фильтры и наложить их" — это самое компактное решение.

С другой стороны, в вашем случае мы не можем абстрагироваться от dbContext, отсюда:
1. Сложность в написании unit тестов для бизнес логики без заведения тестового контекста и соответсвенно тестовой базы данных.(в рамках EF6, в 7 версии придумали MemoryContext)
2. Использовать Ioc-контейнер, то есть нам придется напрямую писать какой контекст мы берем.
3. Писать using(dbContext) для сборки мусора, что в какой-то мере утомительно.

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

Почему не можем?

Я имел ввиду без написания дополнительной обертки, то есть мы везде вынуждены писать:
var context = new SaleDbContext();

так как напрямую обращаемся к DbSet контекста.

Ну так зачем так делать-то? У меня в коде такого нет, там обращение к _dbContext, которая прекрасно может быть типа IDatabaseContext со свойством IQueryable Projects (это если вы не ленивый, и не ходите через Set<T>). А дальше все прелести DIP ваши.

Я, возможно, чего-то не понимаю, но разве AllActiveCustomers — это отдельное самостоятельное бизнес-понятие?
Customer — это бизнес-сущность, Active — это ее бизнес-характеристика, All вообще связано с количеством и применяется уже к результату. Соответственно, наиболее близким к бизнес-логике будет какой-то такой вариант:
CustomerRepository.Find().Active().All()

Active() содержит бизнес-логику кого считать активным — у кого статус «активнен», или у кого активные контракты есть, или что-то еще. А в какую последовательность SQL-команд она потом превращается, это уже детали реализации.
«AllActiveCustomers» я взял из комментария выше, я бы так не назвал. В общем случае метод начинается с глагола, GetActiveCustomers.

> CustomerRepository.Find().Active().All()

.All() — бессмысленная штука. Не знаю что вы в ней писать решили.

.Active() — имеет смысл выделять, при отсутствии инкапсуляции этой логики в объекте. Почему вам может это придти в голову запрашивать из репозитория по какому-то правилу из бизнес-логики? Не знаю. Но, если там есть что выделять, то в БД это так просто не проброситься.

CustomerRepository.Find() — что возвращает?
Если IQueriable — вы получите описанные грабли generic-репозитория (который не репозиторий и никто не знает зачем он такой нужен).
Если коллекцию — вы получите *хрилион не нужных объектов.
Если какой-нибудь ICustomerRepositoryQueryBuilder — задолбаетесь писать под эти билдеры на втором десятке.

В общем, мне страшно представить что вы хотите нагромоздить, вместо одного метода с DB.Customers.Where(x => x.IsActive).
Вопрос иного характера, но мне интересно, как в случае с DB.Customers.Where(x => x.IsActive) можно замокать данную выборку в тестах, если не иметь метода в неком интерфейсе? Мне хотелось бы избежать настройки данных в тесте.
Для данного случая я сам вижу только выделение этого в метод интерфейса какого-то DAO.
DB.Customers.Where(x => x.IsActive) лежит в репозитории, за интерфейсом репозитория. Просто одним методом.
> Я, возможно, чего-то не понимаю, но разве AllActiveCustomers — это отдельное самостоятельное бизнес-понятие?

ActiveCustomers — отдельное.Иначе зачем вам выбирать только их?

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

И абсолютно не связанное с InactiveCustomers и просто Customers? Иначе зачем вам его от них отделять?

Active — это отдельная характеристика, она не образует новых сущностей, а ограничивает подмножество существующих. То есть используется как фильтр. И так как в бизнес-требованиях фильтры могут комбинироваться (активные + юридические лица + из региона Урал), логично выделить их в отдельные методы и аналогично комбинировать в коде.

Так что да, специализированный Query Builder как вариант, а All заканчивает построение и вычисляет результат. А еще иногда бывают нужны не все, а только часть (первые 10 с самыми большими контрактами).
> Active — это отдельная характеристика, она не образует новых сущностей, а ограничивает подмножество существующих

Что, по-вашему, образует? Всё в этой жизни наследники Object, и только характеристиками определяется.

Давайте так: «совершеннолетний» — это термин из ubiquitous language, бизнесовая «характеристика», в терминах которой я общаюсь в коде. А «17 лет» — характеристика объекта.

Фильтровать по возрасту вы можете как угодно, но это не противоречит тому, что репозиторий вам может возвращать совершеннолетних.

> И так как в бизнес-требованиях фильтры могут комбинироваться (активные + юридические лица + из региона Урал), логично выделить их в отдельные методы и аналогично комбинировать в коде.

Когда и если эта комбинаторика нужна — можно выделить. А можно и нет (а, например, построить Query object тем же билдером). А можно эту категорию назвать «Уральские» и не придумывать, что нужны произвольные комбинации.

В остальных 99% Query Builder — адский оверхед на разработку. Ну и к «репозиторию» отношения не имеет.
А почему не нормально? Разве что Load неудачное название, лучше что-то вроде getAll(). Репозиторий для клиента — это коллекция по сути. Добавление или удаление элемента из коллекции — обычная операция.

Лично я являюсь противником generic CRUD репозиториев и предпочитаю создавать бизнес методы в репозитории, например FindAllActiveCustomers.
Я понимаю, что в этом случае часть бизнес логики уезжает в репозиторий, но, на мой взгляд, это лучше, чем ad-hoc linq запросы в бизнес методах. К тому же, есть вероятность, что сложная выборка переедет в базу, и лучше инкапсулировать ее в репозитории. Да и тесты писать проще, т.к. не нужно мокать данные так, чтобы не обрезало фильтром.

>> Выполняет ли репозиторий ту же роль, что и ранее? Очевидный ответ «НЕТ», так как теперь он не извлекает данные из хранилища.

репозиторий абстрагирует от хранилища, а не непосредственно извлекает. Извлекать он может через цепочку любой длины (с кэшированием, логгером итд). Непосредственно извлекает например data mapper/http client/api client. На выходе мы получаем все равно одно и то же — сущности.
В случае с EF мне больше всего нравится разделять доменную модель Entities и маппинг их на базу данных на разные проекты. Так как домен ничего не должен знать о том как его сохраняют и прочее. При этом используются «богатые» модели с логикой. В этом плане мне очень нравится как все раскладывает Jimmy Bogard здесь: vimeo.com/43598193
Выполняет ли репозиторий ту же роль, что и ранее? Очевидный ответ «НЕТ», так как теперь он не извлекает данные из хранилища.

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

В общем, так и есть. Но некоторые считают, что репозиторий не для абстракции хранилища данных, а для абстракции доступа к данным. Из-за этого разница в понимании целей возникает. О чем я и написал.

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

Он такая же абстракция над доступом к данным, как бизнес-объект. У него выхода нет и по цепочке ответственностей, конечно, он абстрагирует. Но куда поместить, например, поместить джойны, генерацию запросов и куда, скажем, системную нотификацию об операциях? Обычная связка: linq-запрос по EF Code First + бросить в очередь сообщение, которое отправится потом другой службой. Что есть что тут и как построить систему. Люди сваливают все это в одно место, не заботясь о разделении, когда думают, что все это — работа с данными. По факту, есть 1) linq-запросы с EF, 2) нотификатор и, что неочевидно некоторым, 3) логика в репозитории создающая сложные BO. Если смотреть с точки зрения тестирования этого всего, то 1) надо будет отделить от 3).
Возможно, частный случай, но, как мне кажется тут и возникает нестыков. Одни полагают, что можно все свалить в одно место, т.к. собираются тестировать руками, а не тестами.
Следите за мыслью?
Он такая же абстракция над доступом к данным, как бизнес-объект

Бизнес-объект — не абстракция над доступом к данным.


Но куда поместить, например, поместить джойны, генерацию запросов и куда, скажем, системную нотификацию об операциях?

Генерацию запросов надо помещать за репозиторий. Дальше все зависит от сложности приложения. С точки зрения пользователя запросы генерируются где-то внутри репозитория, а где — не важно.


системную нотификацию об операциях?

А это зависит от природы "системной нотификации", очевидно.


Следите за мыслью?

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

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

Про BO — это пример эскалации абстракции, из-за которой репозиторий является, по вашим словам, абстракцией над доступом к данным. Кратко, А использует B, использующее С => A использует С.

Генерацию запросов надо помещать за репозиторий.

Верно. И это делает EF, например.

По-моему, мы об одном и том же.

Если рассматривать детально, то одного поста не хватит.

А если детали не рассматривать, то не о чем и говорть.


Кратко, А использует B, использующее С => A использует С.

Это — протекшая абстракция. Если B абстрагирует C, то A ничего не знает про C.


Верно. И это делает EF, например.

… и? Никогда не писали репозитории на EF?

Cчитаю очень плохой идеей запихивать в репозитарий логирование и кэширование — их надо в слой выше.
В некий бизнес-объект?
Вы считаете это элементы бизнес-слоя?
сервис приложения не является частью бизнес-слоя.
Вы про сервис из DDD?
сервис сервисного слоя например — прокладка между реквестом и бизнесом-слоем, инкапсулирующая в себе инфрастуктурные моменты типа получить сущности, закэшировать, залогировать (в контексте нашей ветки).
http://martinfowler.com/eaaCatalog/serviceLayer.html
понял. согласен с тем, что выше.
добавлю тут, а то не дает отправлять комменты чаще раз в 5 мин.

то есть сервис приложения знает о том как работает приложение и может оперировать понятиями типа «если в кэше есть, достанем из кэша, а если нет, то вытащим из хранилища и закэшируем». А репозиторий все-таки — хранилище, поэтому должен в чистом виде только уметь доставать из хранилища и класть в хранилище.
Спорно. Я обращаюсь к репозиторию чтобы получить какой-то объект, мне всё равно откуда он его достанет. Он отвечает за хранение и получение данных, а не какой-то сервис. Где он хранит (в базе, в кэше, в файлах, в облаке и т. д.) мне как клиенту репозитория всё равно.
все верно. Если кэш выступает единственным хранилищем (CacheRepository), то хранилище оно и хранилище. Если же у нас например PostgresRepository с функцией кэширования, то репозиторий перестает быть ЧИСТОЙ абстракцией над хранилищем, т.к. получает еще одну ответственность — оптимизация с помощью кэша. В этом случае мы получаем сайд-эффект, когда запросив из хранилища сущность, мы получили ее вовсе не из хранилища, да и не ее, а старую версию.
В то время как сервис принимает эти риски, т.к. работает именно на уровне самого приложения, где можно применить кэширование и принять сайд-эффект за должное.
Репозиторий не перестаёт быть чистой абстракцией над физическим хранилищем, потому что он ею никогда не был. Его задача предоставить остальной модели коллекцию объектов. Как он это будет делать — детали конкретной реализации. Она может быть оптимизированной, а может быть тупой, а может быть конфигурируемой как глобально, так и параметрами вызова (но это обычно означает текущую абстракцию).
>> Репозиторий не перестаёт быть чистой абстракцией над физическим хранилищем, потому что он ею никогда не был.

как не был, если репозиторий и есть прослойка между доменом и хранилищем, скрывающая в себе детали реализации? Да, реализация может быть не чистая, а декорирующая в себе например кэширование, но это уже дискуссионно, и лично я бы, кэширование выносил на слой выше — либо декорируя с помощью CacheRepository, либо в сервисе, чтобы репозиторий мне возвращал всегда валидные данные непосредственно из хранилища.

Аналогично, если писать http-клиент к стороннему апи, и в него заложить функцию кэширования. Пусть http-клиент всегда будет мне возвращать респонс непосредственно http-реквеста, а кэширование, если надо, я подниму на уровень выше, завернув клиент в сервис.
В определении репозитория нет ничего, чтобы указывало на его чистоту. Это не учитывая того, что кэширование может быть на многих уровнях за репозиторием, в том же DataMapper.
В определении не может быть подопределений — чистого и нечистого. Само по себе определение и есть то, чем репозиторий должен заниматься — Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects (http://martinfowler.com/eaaCatalog/repository.html) — прослойка между доменом и мапперами
Поэтому пусть репозиторий занимается тем, чем должен, а оптимизацией занимается слой приложения.

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

Вот только в слове "mediates" может быть все, что угодно. Например (цитата из того же PoEAA):


It’s also conceivable, when the application is running normally, that certain types of domain objects should always be stored in memory. One such example is immutable domain objects (those that can’t be changed by the user), which once in memory, should remain there and never be queried for again.
ок. Собственно все уже обсудили и донесли друг до друга. Я предпочитаю несколько сущностей с одной узкой ответственностью против одной более функциональной. Поэтому выделяю у сущности задачу, вынося подзадачи в декораторы/сервисы/другие уровни.
Я предпочитаю несколько сущностей с одной узкой ответственностью против одной более функциональной

Я тоже. Но ни мое/ваше предпочтение, ни предпочтение разработчика, предпочитающего одну более крупную сущность, не меняет того, что кэширование не противоречит репозиторию как паттерну.

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

Проблема как раз в этом "другом уровне". Вот у меня есть сервис (доменный), считающий скоринг для запроса на кредит. На входе — сущность запроса, на выходе — сущность ответа (варианты кредитов со скорингом и объяснениями). Ему для работы нужно лазить за другими доменными же сущностями (например, другими клиентами с их кредитной историей). В лучших традициях DIP, он зависит только и исключительно от интерфейсов IЧто-то с методами ДайМнеЧтоТо(критерий). По DDD, эти интерфейсы — это репозиторий. Так на какой "другой уровень" вынести кэширование таким образом, чтобы скоринг-сервис об этом не знал?

где у вас реквест от юзера попадает в домен?
по ddd вы принимаете в Presentation слое реквест, передали его в application — сервисы или хэндлеры cqrs, — а внутри уже оперируете доменным слоем. Чтобы скоринг-сервис об это ничего не знал, доменный сервис можно а) декорировать сервисом приложения б) опять же в домене оставить интерфейс, а реализовать там же где реализованы репозитории — в инфраструктурном слое, — учитывая потребности приложения с кэшем.

Еще раз: я считаю, что кэш — это ответственность приложения (в терминологии ddd). Домен — это бизнес и чистые данные без сайд-эффектов.
где у вас реквест от юзера попадает в домен?

Это не имеет значения.


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

Скоринг-сервис — и есть доменный сервис. Который зависит от других доменных объектов. Откуда ему эти объекты получить?

я все написал комментом выше.

"Комментом выше" нет ответа на вопрос "откуда доменный сервис получает объекты". Ну или я его не вижу, в этом случае, повторите его, пожалуйста.

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

Какого интерфейса?


На пальцах:


class ScorerService: IScorerService
{
  IX _smthToGetDataFrom;

  Scoring Score(CreditApplication a)
  {
    _smthToGetDataFrom.Get<BankClient>(someId);
    _smthToGetDataFrom.Get<BankClient>(someOtherId);
  }
}

Какой паттерн экспонирует IX? (BankClient — доменная сущность)

IX _smthToGetDataFrom — тут может быть например CacheBankClientRepository, декорирующий BankClientRepository

Ну то есть, если предполагать, что мы используем честный DIP, IX — это IBankClientRepository, правильно?

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

… а это, в свою очередь, означает, что ScorerService не может знать, будет ли за IBankClientRepository кэширующая реализация или обычная; что означает, что кэширование происходит на том же уровне, что и репозиторий — уровне реализации интерфейса IBankClientRepository. Более того, поэтому же мы больше не можем говорить, что репозиторий гарантированно не имеет побочных эффектов или возвращает актуальные данные (это нарушает LSP).

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

Частью моей реализации является зависимость от репозитория, причем, согласно DIP, от интерфейса репозитория.


реализовывайте так, чтобы не было сайд-эффектов.

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

>> Частью моей реализации является зависимость от репозитория, причем, согласно DIP, от интерфейса репозитория.

вы несколько раз сделали упор на слове «интерфейс». Когда я пишу про зависимость от чего-то, я всегда имею в виду зависимость от интерфейса.

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

очевидно, что есть случаи когда кэширование нужно, а есть случаи когда кэширование не нужно. Поэтому я кэширование выношу на уровень приложения — туда, где все известно о юзкейсах. Этот сервис (приложения) нужен для подсчета критичных данных — тут кэширование не нужно. Этот сервис нужен для извлечения сущности и показа ее во вьюшке — тут кэширование нужно. Ни репозитории, ни доменные сущности/сервисы не знают о юзкейсах, т.к. оперируют бизнес-понятиями.
доменные сущности/сервисы не знают о юзкейсах, т.к. оперируют бизнес-понятиями.

Юз-кейс — это и есть бизнес-понятие.


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

Прекрасно, и как же вы прокидываете это кэширование внутрь доменных объектов/сервисов, которым оно нужно? Не закэшированные объекты, а именно сам факт кэширования? Повторюсь: скоринг-сервис в моем примере инкапсулирует конкретную бизнес-логику, и только он знает, какие объекты ему понадобятся для ее реализации.

>> Прекрасно, и как же вы прокидываете это кэширование внутрь доменных объектов/сервисов, которым оно нужно?

еще раз: кэширование — ответственность приложения. Домен о кэшировании ничего не знает и знать не должен.

Именно поэтому домен должен получить нечто (функцию id -> объект, если угодно), которая будет давать ему данные, про которые он не знает, закэшированы они, или нет. И вы не поверите, но в DDD такая функция называется репозиторием.

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

Никто не требует, чтобы она была чистая.


Да, реализация может быть не чистая, а декорирующая в себе например кэширование, но это уже дискуссионно, и лично я бы, кэширование выносил на слой выше — либо декорируя с помощью CacheRepository,

Вот это декорирование — по определению декорирования — для пользователя прозрачно. Пользователь все так же идет в IRepository, совершенно не зная, что он получает не SqlRepository, а Cache<SqlRepository>. И именно поэтому пользователю все равно, реализовано кэширование в SqlRepository или в Cache<TRepository>.


Пусть http-клиент всегда будет мне возвращать респонс непосредственно http-реквеста, а кэширование, если надо, я подниму на уровень выше, завернув клиент в сервис.

Тем не менее, в дизайне System.Net.Http.HttpClient сделано наоборот — у вас есть внешний HttpClient, под которым лежит управляемая цепочка message handlers, каждый из которых может быть и кэширующим, а в конце цепочки, собственно, HttpClientHandler/WebRequestHandler, который отвечает за "чистый" запрос/ответ.

>> Вот это декорирование — по определению декорирования — для пользователя прозрачно. Пользователь все так же идет в IRepository, совершенно не зная, что он получает не SqlRepository, а Cache. И именно поэтому пользователю все равно, реализовано кэширование в SqlRepository или в Cache.

Причем тут пользователь? Мы о проектировании с т.з. разработчика.

>> Тем не менее, в дизайне System.Net.Http.HttpClient сделано наоборот — у вас есть внешний HttpClient, под которым лежит управляемая цепочка message handlers, каждый из которых может быть и кэширующим, а в конце цепочки, собственно, HttpClientHandler/WebRequestHandler, который отвечает за «чистый» запрос/ответ.

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

"Пользователь" репозитория — это разработчик, который его вызывает.


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

Вот здесь так и есть: два мелких класса в качестве фронт- (HttpClient) и бэк- (HttpClientHandler) эндов, и мелкие же — и легко повторно используемые! — хэндлеры между ними. Очень простая композиция, по сути, функциональный конвеер.

у нас есть один клиент (сервис), внутри которого цепочка хэндлеров (кэширование, логгеры) и непосредственно клиент, делающий запросы (репозиторий).

У вас нарушилось соответствие аналогии (к сожалению, это регулярная проблема с аналогиями). HttpClientHandler — это не "репозиторий", это аналог клиента БД или ORM, с одной операцией Send (ну или ExecuteCommand). А вот внешний HttpClient — это как раз "репозиторий", с удобными Get, Put, Delete, ReadAs и так далее.

у меня ничего не нарушилось — я не проводил аналогии, а показал, предлагаемую композицию на примере клиента как абстрактного декоратора нескольких сущностей. Вариантов композиций может быть несколько. Например
+ сервис (HttpClient как декоратор всего)
++ кэш (мидлвари между реквестом и респонсом клиента)
++ логгер (мидлвари между реквестом и респонсом клиента)
++ репозиторий (непосредственно клиент делающий запрос, видимо HttpClientHandler в вашей терминологии)
+++ маппер (транспорт — curl, stream итд)

либо
+ сервис
++ репозиторий
+++ кэш
+++ логгер
+++ маппер

Я за первый вариант — за репозиторий, отдающий состояние без сайд-эффектов. Вы за маппер, отдающий состояние без сайд-эффектов. Мы разговариваем только об уровне. Поэтому вы называете маппер HttpClientHandler'ом (сущностью, отдающей результат без сайд-эффекта), а я репозиторий.
А аналогия у нас у каждого своя — в контексте желаемой композиции.

Проблема в том, что у вас сверху везде сервис, в то время как по DDD мы получаем объекты из репозитория.

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

Тот сервис, который у вас сверху списка — это сервис приложения?

… и он напрямую скидывает запросы в кэш, логгер, репозиторий, маппер, блаблабла. Никакого домена — ни в виде сущностей, ни в виде сервисов.

как связаны кэш, логгер и домен? кэширование и логирование — это ваш бизнес-домен? Это все служебные фичи — уровень слоя Приложение или Инфраструктура, если мы делаем обертки репозиториев.

Никак не связаны, про что и разговор: в вашей схеме есть application service, но вместо того, чтобы взаимодействовать с доменом, он взаимодействует с инфраструктурой. Более того, вы взяли и заменили репозиторий — который является внутренним сервисом приложения — на application service, который, по определению, внешний.

application service взаимодействует с доменом через репозиторий и доменные сервисы. Какое еще взаимодействие с доменом вы хотите?

>> Более того, вы взяли и заменили репозиторий — который является внутренним сервисом приложения — на application service, который, по определению, внешний.

я не менял. Каждый на своем уровне занимается своими задачами.
http://i.stack.imgur.com/jYvXp.png
application service взаимодействует с доменом через репозиторий и доменные сервисы. Какое еще взаимодействие с доменом вы хотите?

Доменных сервисов у вас в схеме нет, их мы вычеркиваем. А через репозиторий с доменом взаимодействовать нельзя, можно только получить доменный объект, и потом взаимодействовать с ним. А дальше возникает тот же вопрос, что и выше: application service знает о том, что вокруг репозитория есть кэш, или нет?

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

>> вопрос, что и выше: application service знает о том, что вокруг репозитория есть кэш, или нет?

в моем приложении сервис будет выглядеть так:

collection = cacheService.getByKey(key)
if (collection is null) {
collection = repository.find(id)
cacheService.set(key, collection)
}

// дальше логирование, доменные сервисы и т.д.

То есть application service знает про то, что мы кэшируем доменные объекты. Прекрасно, как теперь всем остальным доменным объектам воспользоваться этой же логикой кэширования?

еще раз: кэширование — задача приложения. домен — про бизнес-задачи, а не служебные. Домен про кэширование ничего не знает.
еще раз: кэширование — задача приложения

Нет. Кэширование полученный из БД объектов — задача слоя, находящегося между доменом и БД. К приложению (в значении application) это отношения не имеет.


Домен про кэширование ничего не знает.

Конечно, эта логика инкапсулируется в инфраструктурный слой.

>> Нет. Кэширование полученный из БД объектов — задача слоя, находящегося между доменом и БД. К приложению (в значении application) это отношения не имеет.

задача слоя, находящегося между доменом и БД — вытащить данные из БД и кинуть их в домен.
задача слоя, находящегося между доменом и БД — вытащить данные из БД и кинуть их в домен.

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

вы видимо про репозиторий? я же про маппер, т.к. репозиторий — это часть домена. Задача маппера — вытащить данные и отдать их репозиторию в виде доменных сущностей.

Я про репозиторий, это явно написано в моем комментарии.

>> Нет. Кэширование полученный из БД объектов — задача слоя, находящегося между доменом и БД. К приложению (в значении application) это отношения не имеет.

между доменом и БД — маппер. Репозиторий — часть домена.
Репозиторий — часть домена.

Это, скажем так, весьма неоднозначное утверждение.

ну не знаю чем оно неоднозначно. DDD — про домен. Домен: сущности, репозитории, VO, сервисы, агрегаты. Репозитории и сервисы в доменном слое представлены интерфейсами. Реализации — в инфраструктуре. Презентация — то, что видит клиент. Приложение — связь презентации и домена с помощью инфраструктуры.

Вернон, Эванс.

Ладно, спасибо за диалог. Надо и честь знать.
Репозитории и сервисы в доменном слое представлены интерфейсами. Реализации — в инфраструктуре.

Вот именно поэтому и неоднозначное: у вас интерфейс в домене, а реализация в инфраструктуре.

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

инвалидируйте в CacheRepository или в CacheService. Чтобы не было размазывания, держите кэш в одном месте — вариантов реализаций много. Я лишь про уместность кэша в домене или в приложении.

Ну так если представить, что httpclient — это интерфейс репозитория, то все так и получается — внутри добавлена цепочка миддлвеа, добавляющая функциональность без изменения интерфейса.

А может реализация в домене, но использует инфраструктуру как зависимости?
Пользователь репозитория — это и есть разработчик.

Некая путаница возникает из-за того, что есть два смысла "репозитория" — как интерфейс для доступа к бизнес объектам и как реализация этого интерфейса при помощи, к примеру, EF. И если запихивать кеширование и аудит в реализацию вперемешку с доступом к данным — это, очевидно, плохая идея, то сделать, например, кеширующий или логирующий прокси репозитория — это нормально, как по мне.

Вы о том, чтобы одна реализация репозитория занималась получением данных из базы, а другая, например, кеширование? Т.е. в BLL дергается некий интерфейс, за которым на самом деле спрятана цепочка репозиториев, которые делают каждый свое. Об этом?
Скорее о том, что «репозиторий» как «черный ящик» должен просто отдавать и принимать данные. Остальное нас не волнует. По SOLID «репозиторий» — это отдельный модуль, декомпозиция которого имеет смысл только в контексте этого модуля.

То же самое, что декомпозиция коровы на бифштекс, котлеты, филе, вымя и ливер имеет смысл только в контексте кулинарии. За приделами кулинарных обсуждений корова — это рогатое травоядное животное, дружелюбное, полезное, но немного сумасбродное, которое может и на рога поднять. И нам не особо важно, какая из частей коровы нежнее и мясистее, если нас интересуют молочный и эстетический интерфейсы.
А если использовать CQRS и аудит решать с помощью событий бизнес логики? Теоретически и актуализацию кэша можно повесить на те же события — тогда write model будет простая ( только CRUD ).
Sign up to leave a comment.

Articles