Pull to refresh

Comments 31

Однако за многолетний опыт разработки, побывав в нескольких компаниях, сменив кучу проектов я НЕ ВСТРЕЧАЛ паттерн "Спецификация" совместно с паттерном "Репозиторий".

Сочувствую вашему профессиональному опыту, думаю что все таки репозиторий с спецификацией часто объединяют (если не ошибаюсь даже в видео Дмитрия Нестерука спецификация через фильтрацию к базе иллюстрирована), вот одмн из опенсорс проектов: https://github.com/NikitaEgorov/nuSpec

Отличная библиотека, кстати.

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

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

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

куча не всегда нужных не универсальных методов

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

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

А зачем такая прокладка над dbset-ами?

То, что юзера нельзя удалять это не костыль, так как на него каскадно ссылается много чего. А если мы ещё и с деньгами дело имеем, то выпиливать контрагента совсем не стоит

Удаление пользователя легко запретить, наследуя конкретный IUserRepostory не от IGenericRepository<User>, а от ICanRead<User>, ICanUpdate<User>, GenericRepository<User>, но не ICanDelete<User>, и где GenericRepository<> - скрытый от потребителя тип.

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

Переопределить метод удаления в реализации репозитория пользователя, не?

Тогда получишь просто гениальный код вида

await DeleteAsync<User>(userId); //nothing happent

И это все еще не объясняет, зачем делать обертку из "паттерна" спецификация над "паттерном" репозиторий, которые в свою очередь оборачивают dbset, который уже является репозиторием и ef, как спецификация к нему.

А за чем делать свою дополнительную обертку, если в языке уже есть встроенная реализация паттерна в виде Expression? С репозиторием аналогично - любая ормка же предоставляет реализацию паттерна из коробки.

Согласен, а сам ванильный паттерн выглядит невероятно громоздким на этом фоне.

Я использовал подобный подход и по итогу запросы из репозитария расползаются по всей программе и реализованы немного по разному. За этим нужно следить.

В итоге использую всевозможные построили запросов только внутри репозитария. Это внутреннее дело репы, как оно там всё хранится и как строить запросы. Но у меня своя специфика в виде даппера.

Чтобы справиться с комбинаторным взрывом использую концепцию фильтров, когда null обозначает отсутствие фильтра. Например: GetEmployees(Sex? sexFilter = null, int? minAgeFilter = null, ...). При вызове явно указываю названия использованных фильтров, чтобы не было GetEmployees(false, 24).

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

Вижу некоторое лукавство. Если от интерфейса/фронта приходит фильтр, то его всё равно надо будет собрать в спецификацию. И код со спецификациями получится очень похожим по структуре на код BadManRepository2.

В конечном счёте, разве IQueryable - это не реализация паттерна спецификация?

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

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

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

Предложенная вами система с спецификациями это создание новой абстракции поверх существующей (ОRM) поверх существующей (SQL). Но ради каких профитов? Ваши плюсы не являются плюсами перед существующим подходами:

> использование абстракций для доступа к данным – правильное решение;

Репозиторий как паттерн - это и есть абстракция для доступа к данным. Она инкапсулирует в себя низкоуровневые запросы к дравйверу бд и возвращает доменную модель.

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

Как будут выглядеть запросы с join? Кажется, как будто здесь также придется делать отдельные non-generic методы для сложных запросов, в итоге вся унификация теряется.

> Добавление разных вариаций запросов данных сводится к созданию одной  строки кода;

В вашем же примере BadManRepository2 это делается точно также - вы меняете Request модель, и затем добавляете 1 строчку изменений в репозиторий (Where).

> изменение запросов производится на уровне пользовательского кода — нет необходимости менять код репозитория;

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

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

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

var query = CreateQuery();

return query.FirstOrDefault();

Объект query сам по себе неплохая спецификация.
Только использовать его лучше так.

class ManService {
  public void doSomething() {
    var query = CreateQuery();
    query = query.Where(x => x.departmentId == 123);

    List<Man> manList = this.manRepository.findByQuery(query);
  }
}

В этом случае можно менять реализацию manRepositоry как угодно, он не обязательно будет привязан к SQL. Я так понимаю, у вас примерно это и получилось, repository.Get(spec2) это аналог findByQuery.
Если query в используемой ORM слишком низкоуровневый, то да, можно сделать более абстрактную обертку. Проблема с методами типа WithAge в том, что со временем понадобится не только 'равно', а еще 'больше' и 'меньше', и придется или делать метод на каждую комбинацию, или универсальный механизм задания операторов, и получится тот же самый query builder.

class BadManRepository2 {
  public Man Get(GetManRequest request) {
  }
}

GetManRequest это должно быть входное DTO, где поля заполняются пользователем, список полей соответствует полям ввода пользовательского интерфейса. Оно должно использоваться в сервисе, а не в репозитории.
В приложении не должно быть внутренних Request DTO, они быстро превращаются в God-object со всеми возможными полями и операторами, каждое из которых может быть null. Должны быть или специфичные методы репозитория findBySomething, или query builder.

class ManService {
  public PaginatedResult<Man> list(GetManRequest request) {
    if (request.Name is not null) {
        query = query.Where(x => x.Name == request.Name);
    }

    pageSize = 30;
    query = query.offset((request.Page - 1) * pageSize);
    query = query.limit(pageSize);

    List<Man> result = this.manRepository.findByQuery(query);
    int total = this.manRepository.findByQuery(query->count());
    int totalPages = Math.ceil(total / pageSize);

    return new PaginatedResult(request->page, totalPages, result);
}

Понял, приму во внимание, спасибо

Как скомбинировать спецификации, у которых разные Skip и Take?

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

Скорее всего будут использованы skip и take последней спецификации, у которой они проставлены.

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

Если отвечать на вопрос прямо, при текущей реализации, возможно менять Skip и take извне. Поэтому после объединения двух разных спецификаций, можно поставить необходимые Skip и Take

А есть ссылка на гитхаб с рабочим сэмплом?

Возник вопрос - почему в иерархии наследования репозиториев появляется класс StorageBase<Entity>, хотя совершенно логично его назвать ARepository<Entity>. В чём причина такого названия для базового класса?

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

UML модель брал с реального примера, где использовались названия Storage для репозиториев, и не совсем полностью подправил.

Как реализована комбирнация выражений left.And(right) и left.Or(right)?

Вы можете увидеть это в примерах кода. Обратите внимание на классы AndSpecification и OrSpecification.

В классе Expression<> нет методов And и Or. Они есть в классе Expression, но они статические. Так что код left.And(right) не должен компилироваться, вместо него должно быть написано Expression.And(left, right). Либо есть какой-то экстеншн-метод And, которого нет в коде из статьи. Так что вопрос про комбинацию актуален, в статье ответ на него увидеть нельзя.

И оставшийся без ответа вопрос про рабочий сэмпл кода тоже актуален )

Вы сами ответили на свой вопрос) либо я до сих пор не понимаю вас

В And или Or спецификация (приведённых в статье) в методе SatisfiedBy() для комбинации двух Expression используется вызов left.Or(right)

Эти методы реализованы на уровне Expression

Про рабочий пример - не могу показать, репозитории являются приватными

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

репозитории являются приватными

можно сдерать сэмпл на гитхабе, верно? )

для комбинации двух Expression используется вызов left.Or(right)

такой код не скомпилируется так как метод Or - статический (ни в Expression, ни в Expresions<> нет нестатического метода и это два разных класса)
Expression<Func<int, bool>> left = x => x > 0;
Expression<Func<int, bool>> right = y => y > 0;
var res = left.Or(right);

А вот такой скомпилируется (правда выдаст исключение)
Expression<Func<int, bool>> left = x => x > 0;
Expression<Func<int, bool>> right = y => y > 0;
var res = Expression.Or(left, right);

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

И правда, вы полностью правы, потерял среди файлов, написанное расширение на Expression.

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

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

При нашем анализе факапа выяснили, что цели мы не достигли, ради чего вообще внедряли подход. Заявленные плюсы не работают. Всё дело в том, что бизнес обычно значительно сложнее, чтобы можно было сверстать любой запрос комбинацией спецификаций с помощью And/Or. Немаловажный критерий к качеству продукта, это скорость, отзывчивость -- чему спецификации не способствуют, а только создают ещё один слой проблем для разработчика. Т.е. вместо того, чтобы решать задачу бизнеса, он борется с паттерном и его реализацией, стараясь как-то вот так в раскоряку расширить, иногда путём явных нарушений введённых принципов. А иногда и вовсе задача не решается никак, и приходится, наплевав на слой спецификаций сбоку городить обычный класс с SQL-запросами.

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

Sign up to leave a comment.

Articles