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

Что не так с репозиторием? Очевидно, что с самим паттерном все нормально, но разница в его понимании разработчиками. Я попробовал исследовать это и наткнулся на два основных момента, которые, на мой взгляд, являются причиной разного к нему отношения. Одним из них является «скользящая» ответственность репозитория, а другой связан с недооценкой unit testing. Под катом я объясню первый.

Скользящая Ответственность Репозитория


Когда речь заходит о построении архитектуры приложения у всех в голове сразу возникает представление о трех слоях: слой представления (Presentation Layer), бизнес логики (Business Layer) и слой данных (Data Layer) (см MSDN). В таких системах объекты бизнес логики используют репозитории для извлечения данных из физических хранилищ. Репозитории возвращают бизнес-сущности вместо сырых рекордсетов. Очень частно это обосновывается тем, что если бы нам нужно было заменить тип физического хранилища (база, файл или сервис), то мы сделали бы абстрактный класс репозитория и реализовали бы специфичный для нужного нам хранилища. Выглядит примерно так:

image

О таком же разделении говорит Мартин Фаулер и MSDN. Как обычно, описание — это, всего лишь, упрощенная модель. Поэтому, хотя это выглядит правильно для небольшого проекта, это вводит в заблуждение, когда вы пытаетесь перенести этот паттерн на более сложный. Существующие ORM еще сильнее сбивают с толку, т.к. реализуют многие вещи из коробки. Но представим, что разработчик знает только как использовать Entity Framework (или другой ORM) только для получения даных. Куда, например, он должен поместить кеш второго уровня, или логирование всех бизнес-операций? В попытке сделать это, очевидно, что он будем стараться разделить модули по функционалу, следую SPR из SOLID, и построить из них композицию, которая, возможно, будет выглядеть так:

image

Выполняет ли репозиторий ту же роль, что и ранее? Очевидный ответ «НЕТ», так как теперь он не извлекает данные из хранилища. Эта ответственность была перенесена в другой объект. На этой почве возникает первая нестыковка между лагерями абстракционистов и конкретистов.

Вместо анализа


Если принять за правило, что все термины должна сохранять свое значение вне зависимости от модификаций кода, то было бы правильно на первом этапе не называть репозиторием объект, который просто возвращает данные. Это DAO паттерн чистой воды, т.к. его задача спрятать конкретный интерфейс доступа к данным (ling, ADO.NET, или что-то еще). В то же время репозиторий может не знать о таких деталях вообще ничего, собирая в единую композицию все подсистемы доступа к данным.

Считаете ли вы, что такая проблема существет?
Поделиться с друзьями
-->

Комментарии (160)


  1. VolCh
    06.07.2016 10:32

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

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


  1. indestructable
    06.07.2016 12:07

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


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


    1. ETman
      06.07.2016 13:57

      Проблем с тестированием этого не возникнет? В одной сущности намешано несколько вещей (SPR не соблюдается), как я вас понимаю.


      1. indestructable
        06.07.2016 17:45

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


        1. ETman
          06.07.2016 17:52

          Если у вас в репозитории только linq запрос или вызов хранимки, разве это репозиторий? Это «тупой» DAO.


          1. VolCh
            06.07.2016 18:02

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


          1. lair
            06.07.2016 18:02
            +1

            Если у вас в репозитории только linq запрос или вызов хранимки, разве это репозиторий?

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


            1. ETman
              06.07.2016 18:10

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

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

              ?


              1. lair
                06.07.2016 18:16

                Во-первых, ваши "но не 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);
                }

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


                1. areht
                  07.07.2016 01:31

                  > IQueryable Query();

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

                  Где у вас «methods that select objects based on some criteria»? Где «encapsulating the actual storage and query technology»?


                  1. lair
                    07.07.2016 01:36

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

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


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

                    Вот:


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

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

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


                    1. areht
                      07.07.2016 01:55

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

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

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


                      1. lair
                        07.07.2016 01:59

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

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


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

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


                        1. areht
                          07.07.2016 02:02

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


                          1. lair
                            07.07.2016 02:03

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

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


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


                            1. areht
                              07.07.2016 02:11

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


                              1. lair
                                07.07.2016 02:12

                                В где, простите? (лучше сразу с примером)


                                1. areht
                                  07.07.2016 02:16

                                  в бизнес-логике

                                  FindAllActiveCustomers().Where(...)

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


                                  1. lair
                                    07.07.2016 02:18

                                    FindAllActiveCustomers().Where(...)

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


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

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


                                    1. areht
                                      07.07.2016 02:29

                                      fully instantiated collections of objects whose attribute values meet the criteria

                                      IEnumerable Custumer

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

                                      Тоже метод.


                                      1. lair
                                        07.07.2016 12:32
                                        -1

                                        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, ни в провайдерах, всю адаптацию надо писать самому, все надо придумывать самому (например, как сделать, чтобая была честная поддержка времени компиляции). В итоге, проектов, в которых эти вложения оправдаются, не так уж и много.


                                        1. areht
                                          07.07.2016 13:34

                                          > Если буквоедствовать, то 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 превращается в поле с граблями.


                                          1. lair
                                            07.07.2016 13:50

                                            Ну дёрнете ToList от своего IQueriable

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


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

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


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

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


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

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


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

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


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

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


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

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


                                            1. areht
                                              07.07.2016 14:33

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

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

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

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

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

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

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

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

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

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

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

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


                                              1. lair
                                                07.07.2016 14:37

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

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


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

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


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

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


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

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


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

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


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

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


                                                1. areht
                                                  07.07.2016 14:47

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

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

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

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

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

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

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

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

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


                                                  1. lair
                                                    07.07.2016 14:50

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

                                                    Медленно.


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

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


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

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


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

                                                    Your mileage may vary.


                                                    1. areht
                                                      07.07.2016 14:58

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

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

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

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


                                                      1. lair
                                                        07.07.2016 15:02

                                                        Если там dc.Customers.Where(x => x.IsActive).ToList() — аналитически (никак).

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


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

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


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

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


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


                                                        1. areht
                                                          07.07.2016 15:14

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

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

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

                                                          Что?


                                                          1. lair
                                                            07.07.2016 15:21

                                                            Что?

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


                                                            1. areht
                                                              07.07.2016 15:42

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

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

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


                                                              1. lair
                                                                07.07.2016 15:44

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

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


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

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


                                                                1. areht
                                                                  07.07.2016 15:53

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

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

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

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


                                                                  1. lair
                                                                    07.07.2016 15:55

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


                                                                    1. areht
                                                                      07.07.2016 16:04

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

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

                                                                      Вы таки любите передергивать.


                                                        1. areht
                                                          07.07.2016 15:48

                                                          > Понятно, что выставленный наружу 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 ли это?»

                                                          Дело тут не в формализме, это просто совсем другой подход.


                                                          1. lair
                                                            07.07.2016 15:50

                                                            Но это не «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 вам обойтись (в современных условиях) не выйдет. А вот зачем нужны репозитории — это очень разумный вопрос.


                                                            1. areht
                                                              07.07.2016 15:59

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

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

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

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


                                                              1. lair
                                                                07.07.2016 16:00

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

                                                                Я тоже.


                                        1. indestructable
                                          07.07.2016 14:15

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

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


                                          1. lair
                                            07.07.2016 14:19

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


                                            1. indestructable
                                              12.07.2016 12:43

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


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


                                              1. lair
                                                12.07.2016 12:53

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

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


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

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


                                                1. indestructable
                                                  12.07.2016 13:56

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

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


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

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


                                                  1. lair
                                                    12.07.2016 14:04

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

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


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

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


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

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


                                                    1. indestructable
                                                      12.07.2016 15:40

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

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


                                                      1. lair
                                                        12.07.2016 17:24
                                                        +1

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

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


                                                        1. indestructable
                                                          12.07.2016 18:09

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


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


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


                                                          1. lair
                                                            12.07.2016 18:26

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


                                                        1. ETman
                                                          13.07.2016 12:36

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

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


                                                          1. lair
                                                            13.07.2016 12:40

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

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


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

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


                                                            1. areht
                                                              13.07.2016 12:47

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


                                                              1. lair
                                                                13.07.2016 12:49

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

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


                                                                1. areht
                                                                  13.07.2016 13:00

                                                                  Поэтому я и не люблю экстеншены, да.


                                                            1. ETman
                                                              14.07.2016 00:22

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

                                                              А что такое AST?

                                                              Да, кстати, этот IQueryProvider, по-сути, и есть DAO. Не так ли?


                                                              1. lair
                                                                14.07.2016 00:25

                                                                Но все же мне кажется сильно универсального тут не сделать.

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


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

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


                                                                А что такое AST?

                                                                Abstract syntax tree.


                                                              1. indestructable
                                                                14.07.2016 16:07

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


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


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


                                                                1. lair
                                                                  14.07.2016 16:48

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

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


                                                                  1. indestructable
                                                                    14.07.2016 17:30

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


                                                                    1. lair
                                                                      14.07.2016 17:33

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


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


                                    1. Pavel_Develop
                                      07.07.2016 09:29

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


                                      1. lair
                                        07.07.2016 12:46
                                        +1

                                        В моем опыте и личном мнении обобщенный репозиторий поверх хорошего 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>();
                                            }


                                        1. Pavel_Develop
                                          07.07.2016 14:12

                                          Благодарю за ответ.

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

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

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


                                          1. lair
                                            07.07.2016 14:18

                                            Получается вы предлагаете проблему сложных выборок решить extensions-классами

                                            Extension-методами.


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

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


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

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


                                            1. Pavel_Develop
                                              07.07.2016 16:11

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

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


                                              1. lair
                                                07.07.2016 16:13

                                                С другой стороны, в вашем случае мы не можем абстрагироваться от dbContext,

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


                                                1. Pavel_Develop
                                                  07.07.2016 16:42

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

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


                                                  1. lair
                                                    07.07.2016 16:44

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


                          1. michael_vostrikov
                            12.07.2016 18:20
                            +1

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

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


                            1. areht
                              12.07.2016 18:52

                              «AllActiveCustomers» я взял из комментария выше, я бы так не назвал. В общем случае метод начинается с глагола, GetActiveCustomers.

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

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

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

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

                              В общем, мне страшно представить что вы хотите нагромоздить, вместо одного метода с DB.Customers.Where(x => x.IsActive).


                              1. ETman
                                13.07.2016 10:38

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


                                1. areht
                                  13.07.2016 12:39

                                  DB.Customers.Where(x => x.IsActive) лежит в репозитории, за интерфейсом репозитория. Просто одним методом.


                            1. areht
                              12.07.2016 19:17

                              > Я, возможно, чего-то не понимаю, но разве AllActiveCustomers — это отдельное самостоятельное бизнес-понятие?

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

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


                              1. michael_vostrikov
                                12.07.2016 20:58

                                ActiveCustomers — отдельное

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

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

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


                                1. areht
                                  12.07.2016 21:22
                                  +1

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

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

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

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

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

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

                                  В остальных 99% Query Builder — адский оверхед на разработку. Ну и к «репозиторию» отношения не имеет.


              1. VolCh
                06.07.2016 18:17

                А почему не нормально? Разве что Load неудачное название, лучше что-то вроде getAll(). Репозиторий для клиента — это коллекция по сути. Добавление или удаление элемента из коллекции — обычная операция.


              1. indestructable
                06.07.2016 20:32

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


  1. zelenin
    06.07.2016 12:54
    +4

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

    репозиторий абстрагирует от хранилища, а не непосредственно извлекает. Извлекать он может через цепочку любой длины (с кэшированием, логгером итд). Непосредственно извлекает например data mapper/http client/api client. На выходе мы получаем все равно одно и то же — сущности.


  1. Pavel_Develop
    06.07.2016 12:54

    В случае с EF мне больше всего нравится разделять доменную модель Entities и маппинг их на базу данных на разные проекты. Так как домен ничего не должен знать о том как его сохраняют и прочее. При этом используются «богатые» модели с логикой. В этом плане мне очень нравится как все раскладывает Jimmy Bogard здесь: vimeo.com/43598193


  1. lair
    06.07.2016 13:10
    +1

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

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


    1. ETman
      06.07.2016 13:54

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


      1. lair
        06.07.2016 14:08
        -1

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


        1. ETman
          06.07.2016 14:55

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


          1. lair
            06.07.2016 15:03

            Он такая же абстракция над доступом к данным, как бизнес-объект

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


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

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


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

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


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

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


            1. ETman
              06.07.2016 15:20

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

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

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

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

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


              1. lair
                06.07.2016 15:24

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

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


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

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


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

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


                1. ETman
                  06.07.2016 15:58

                  писал


  1. RouR
    06.07.2016 14:26

    Cчитаю очень плохой идеей запихивать в репозитарий логирование и кэширование — их надо в слой выше.


    1. ETman
      06.07.2016 14:36

      В некий бизнес-объект?


      1. zelenin
        06.07.2016 16:56

        в сервис. поддерживаю RouR


        1. ETman
          06.07.2016 17:24

          Вы считаете это элементы бизнес-слоя?


          1. zelenin
            06.07.2016 17:25

            сервис приложения не является частью бизнес-слоя.


            1. ETman
              06.07.2016 17:53

              Вы про сервис из DDD?


              1. zelenin
                06.07.2016 17:58

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


                1. ETman
                  06.07.2016 18:03

                  понял. согласен с тем, что выше.


                  1. zelenin
                    06.07.2016 18:05

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

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


                    1. VolCh
                      06.07.2016 18:13
                      +1

                      Спорно. Я обращаюсь к репозиторию чтобы получить какой-то объект, мне всё равно откуда он его достанет. Он отвечает за хранение и получение данных, а не какой-то сервис. Где он хранит (в базе, в кэше, в файлах, в облаке и т. д.) мне как клиенту репозитория всё равно.


                      1. zelenin
                        06.07.2016 18:22

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


                        1. VolCh
                          06.07.2016 18:30

                          Репозиторий не перестаёт быть чистой абстракцией над физическим хранилищем, потому что он ею никогда не был. Его задача предоставить остальной модели коллекцию объектов. Как он это будет делать — детали конкретной реализации. Она может быть оптимизированной, а может быть тупой, а может быть конфигурируемой как глобально, так и параметрами вызова (но это обычно означает текущую абстракцию).


                          1. zelenin
                            06.07.2016 18:40

                            >> Репозиторий не перестаёт быть чистой абстракцией над физическим хранилищем, потому что он ею никогда не был.

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

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


                            1. VolCh
                              06.07.2016 18:44

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


                              1. zelenin
                                06.07.2016 18:53

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

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


                                1. lair
                                  06.07.2016 19:02

                                  Вот только в слове "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.


                                  1. zelenin
                                    06.07.2016 19:07

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


                                    1. lair
                                      06.07.2016 19:09

                                      Я предпочитаю несколько сущностей с одной узкой ответственностью против одной более функциональной

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


                                      1. zelenin
                                        06.07.2016 19:18

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


                                        1. lair
                                          06.07.2016 20:48

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


                                          1. zelenin
                                            06.07.2016 21:20

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

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


                                            1. lair
                                              06.07.2016 21:40

                                              где у вас реквест от юзера попадает в домен?

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


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

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


                                              1. zelenin
                                                06.07.2016 21:43

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


                                                1. lair
                                                  06.07.2016 21:48

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


                                                  1. zelenin
                                                    06.07.2016 22:02

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


                                                    1. lair
                                                      06.07.2016 22:09

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

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


                                                      На пальцах:


                                                      class ScorerService: IScorerService
                                                      {
                                                        IX _smthToGetDataFrom;
                                                      
                                                        Scoring Score(CreditApplication a)
                                                        {
                                                          _smthToGetDataFrom.Get<BankClient>(someId);
                                                          _smthToGetDataFrom.Get<BankClient>(someOtherId);
                                                        }
                                                      }

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


                                                      1. zelenin
                                                        06.07.2016 22:20

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


                                                        1. lair
                                                          06.07.2016 22:25

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


                                                          1. zelenin
                                                            06.07.2016 22:33

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


                                                            1. lair
                                                              06.07.2016 22:36

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


                                                              1. zelenin
                                                                06.07.2016 22:43

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


                                                                1. lair
                                                                  06.07.2016 22:45

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

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


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

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


                                                                  1. zelenin
                                                                    06.07.2016 22:52

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

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

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

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


                                                                    1. lair
                                                                      06.07.2016 22:55

                                                                      доменные сущности/сервисы не знают о юзкейсах, т.к. оперируют бизнес-понятиями.

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


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

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


                                                                      1. zelenin
                                                                        06.07.2016 22:59

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

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


                                                                        1. lair
                                                                          06.07.2016 23:01
                                                                          +1

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


                            1. lair
                              06.07.2016 18:49

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

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


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

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


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

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


                              1. zelenin
                                06.07.2016 19:00

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

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

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

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


                                1. lair
                                  06.07.2016 19:04

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

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


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

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


                                  1. zelenin
                                    06.07.2016 19:12

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


                                    1. lair
                                      06.07.2016 20:44

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


                                      1. zelenin
                                        06.07.2016 21:05

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

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

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


                                        1. lair
                                          06.07.2016 21:41

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


                                          1. zelenin
                                            06.07.2016 21:51

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


                                            1. lair
                                              06.07.2016 21:57

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


                                              1. zelenin
                                                06.07.2016 22:04

                                                именно


                                                1. lair
                                                  06.07.2016 22:10

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


                                                  1. zelenin
                                                    06.07.2016 22:14

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


                                                    1. lair
                                                      06.07.2016 22:16

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


                                                      1. zelenin
                                                        06.07.2016 22:26

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

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

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


                                                        1. lair
                                                          06.07.2016 22:28

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

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


                                                          1. zelenin
                                                            06.07.2016 22:40

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

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

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

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

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


                                                            1. lair
                                                              06.07.2016 22:42

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


                                                              1. zelenin
                                                                06.07.2016 22:45

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


                                                                1. lair
                                                                  06.07.2016 22:46

                                                                  еще раз: кэширование — задача приложения

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


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

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


                                                                  1. zelenin
                                                                    06.07.2016 22:54

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

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


                                                                    1. lair
                                                                      06.07.2016 22:56

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

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


                                                                      1. zelenin
                                                                        06.07.2016 23:05

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


                                                                        1. lair
                                                                          06.07.2016 23:07

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


                                                                          1. zelenin
                                                                            06.07.2016 23:17

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

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


                                                                            1. lair
                                                                              06.07.2016 23:24

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

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


                                                                              1. zelenin
                                                                                06.07.2016 23:45

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

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

                                                                                Ладно, спасибо за диалог. Надо и честь знать.


                                                                                1. lair
                                                                                  06.07.2016 23:47

                                                                                  Репозитории и сервисы в доменном слое представлены интерфейсами. Реализации — в инфраструктуре.

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


                                                            1. indestructable
                                                              07.07.2016 14:18

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


                                                              1. zelenin
                                                                07.07.2016 14:22

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


                                        1. indestructable
                                          06.07.2016 22:31

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


                                          1. VolCh
                                            07.07.2016 06:38

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


                                1. mird
                                  07.07.2016 10:00

                                  Пользователь репозитория — это и есть разработчик.


        1. lair
          06.07.2016 17:40
          -1

          Вопрос в том, кто этот сервис вызывает.


    1. indestructable
      06.07.2016 17:51
      +1

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


      1. ETman
        12.07.2016 10:18

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


  1. ksuvakin
    06.07.2016 14:55

    А если использовать CQRS и аудит решать с помощью событий бизнес логики? Теоретически и актуализацию кэша можно повесить на те же события — тогда write model будет простая ( только CRUD ).