Обновить

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

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

По поводу «вынести в приватные методы» — если вынести switch в приватный метод того же класса, класс всё равно зависит от dbContextexternalApi и _logService. Тестировать бизнес-правило отдельно от инфраструктуры не получится — моки тех же 5 зависимостей. Суть не в том, чтобы код стал короче, а в том, чтобы бизнес-логика не зависела от инфраструктуры.

Про обратную сторону — это может быть проблемой. Именно поэтому в статье написано «Слоёв нужно столько, сколько нужно. Для простого CRUD — может, и одного достаточно.» Если алгоритм линейный и тестов нет — вынос в 10 классов сделает только хуже. Но когда появляются тесты или меняется инфраструктура (другой банк, другой ORM) — цена «всё в одном месте» резко растёт.

По поводу «вынести в приватные методы» — если вынести switch в приватный метод того же класса, класс всё равно зависит от dbContextexternalApi и _logService. Тестировать бизнес-правило отдельно от инфраструктуры не получится — моки тех же 5 зависимостей. Суть не в том, чтобы код стал короче, а в том, чтобы бизнес-логика не зависела от инфраструктуры.

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

Про обратную сторону — это может быть проблемой. Именно поэтому в статье написано «Слоёв нужно столько, сколько нужно. Для простого CRUD — может, и одного достаточно.» Если алгоритм линейный и тестов нет — вынос в 10 классов сделает только хуже. Но когда появляются тесты или меняется инфраструктура (другой банк, другой ORM) — цена «всё в одном месте» резко растёт.

"Слоёв нужно столько, сколько нужно" да вот только как понять сколько нужно? Это как раз фундаментальная проблема всех архитектур, когда люди начинающие проект думают что их слишком много или слишком мало а потом оказывается что наоборот, тут просело а там наоборот вылезло. И для архитектуры это выглядит как совет "не делай плохо, делай хорошо". Я к тому что что такие советы слишком абстрактны и очень вероятно что человек прочитавший такой совет не сможет применить его в своем проекте потому что ту грань о которой Вы говорите определить очень сложно.

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

Со статическим методом — для чистого switch это сработает, но это процедурный подход. В ООП логичнее инкапсулировать логику в сам объект. Логика принятия решения живёт в самом объекте решения — ни зависимостей, ни моков, ни статических утилит.

“Слоёв нужно столько, сколько нужно” да вот только как понять сколько нужно?

Про «сколько слоёв нужно» — для большинства задач классических трёх слоёв из Clean Architecture хватает. Если становится тесно — стоит делить слой на фичи (vertical slices), а не добавлять новые слои. Да если весь сервис — это «взять из базы, вернуть клиенту» — одного слоя достаточно.

>Со статическим методом — для чистого switch это сработает, но это процедурный подход. В ООП логичнее инкапсулировать логику в сам объект. Логика принятия решения живёт в самом объекте решения — ни зависимостей, ни моков, ни статических утилит.

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

ФП — хорошая альтернатива ООП, если понимаешь его парадигмы и уверенно ими пользуешься. Проблема в том, что для большинства разработчиков «функциональный подход» сводится к «чистым функциям без состояния» — и на этом всё. Без понимания неизменяемости, композиции, алгебраических типов и разделения эффектов получается не ФП, а просто статические методы с процедурным кодом внутри.

А зачем требование "понимать его парадигмы и уверенно ими пользоваться"? В C# большая часть фич функциональных языков на уровне языка не поддерживается. А вот иммутабельность типов поддерживается очень хорошо и в базовых типах (string, DateTime) введена изначально. Функции без side-эффектов на C# писать никто не мешает.

>Проблема в том, что для большинства разработчиков «функциональный подход» сводится к «чистым функциям без состояния» — и на этом всё.

Если разработчик понимает, что такое "чистая функция без состояния", значит он понимает и что такое "неизменяемость" и что такое "разделение эффектов". Так что никакой проблемы в этом нет. Многие вещи на C# вполне могут быть решены в функциональном стиле и мешает разработчикам не их недостаточная погружённость в концепции ФП-языков, а вездесущие евангелисты, толкающие поп-ООП в любую дыру.

Со статическим методом — для чистого switch это сработает, но это процедурный подход. В ООП логичнее инкапсулировать логику в сам объект. Логика принятия решения живёт в самом объекте решения — ни зависимостей, ни моков, ни статических утилит.

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

Про «сколько слоёв нужно» — для большинства задач классических трёх слоёв из Clean Architecture хватает. Если становится тесно — стоит делить слой на фичи (vertical slices), а не добавлять новые слои. Да если весь сервис — это «взять из базы, вернуть клиенту» — одного слоя достаточно.

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

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

Это не золотая пуля, но решает большинство типичных проблем — как и ООП, DDD и десятки других паттернов. Никто не обещает, что применение Clean Architecture автоматически сделает проект хорошим; это инструмент, а не магия.

А на вопрос «почему столько человек делают чёрти что» — ответ прост: незнание. Я работал в разных командах, с людьми, которые технически знали язык куда лучше меня — читали MSDN целиком, разбирались, как GC работает под капотом, оптимизировали код, глядя в IL. Но в архитектуру развиваться большинству было всё равно. И когда я заставлял их следовать правилам в текущем проекте, через полгода-год они уже не понимали, как начать новый проект без Clean Architecture.

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

Я же выше написал что это была ирония а Вы все приняли серьезно.

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

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

Вам надо протестировать этот код юнитами то что за тесты Вы напишите, которые проверяют что из одной пары моков вернется то значение которые Вы пропишите в arrange теста и что оно потом передастся в другой мок другого метода?

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

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

Как раз наоборот. Следуя чёткой структуре, я всегда быстро разберусь, где бизнес-правила, а где их вызов: в application-слое — простая оркестрация, в домене — бизнес-правила. До рефакторинга всё это было в одном методе на 56 строк вперемешку с _dbContext, HttpClient и генерацией URL — вот это через полгода действительно не разберёшь.

Хорошо давайте умножим на реальность Ваш пример. Каждое действие это не одно действие а группа действий которая вполне может состоять из подгруппы и даже подгрупп подгрупп. Допустим получение заказа требует не одного запроса а нескольких, еще и из разных бдшек, наша естественная реакция это разложить все это по разным сервисам, итого мы имеем следующее у нас есть вызов в нашем методе некого сервиса, он внутри вызывает пару других, внутри которых вызываются еще какие-то сервисы. Да все это относиться к заказу и часть целого но по факту эти части обрабатывают разные подразделения и собираются во едино они только во время оплаты. Допустим нам надо знать скидки из отдельной бд, количество товара со склада из еще одной бд, и персональные предложения для пользователя из еще одной бд, ну и вишенка нам надо дернуть есть ли баланс на внутрикорпоративной карте которая в еще какой-нибудь отдельной бд валяется. Итого если мы будем придерживаться во всем принципа выделения инфраструктуры то легко сможем иметь многоуровневые слои вызовов. Еще раз я просто указал что такая проблема возможна если ударяясь в крайности пользоваться данным советом. Сам по себе совет хороший.

Я в стародавние времена взялся читать книгу Roy Osherove "Art of unit-testing", тогда новая для меня была тема. В одном из первых примеров там тестировался сервис, который брал данные из одной зависимости и перекладывал их в другую. Всё это сопровождалось подробным листингом с многострочной настройкой моков. И тут я поймал себя на мысли: а что, собственно, мы тут тестируем? Работу проверяемого метода (system under the test) или настройку моков? Настройка моков была намного сложнее логики сервиса.

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

Юнит-тесты и интеграционные — не конкуренты, а дополняют друг друга. Юнит-тесты быстрые, запускаю десятки раз во время разработки — поменял код, прогнал, увидел результат. Интеграционные через API проверяют систему целиком, но полный прогон на полчаса+ каждый раз не запустишь. И разделение на слои помогает не только с тестированием: когда бизнес-логика в отдельном классе без зависимостей — её проще читать, проще менять, проще передать другому разработчику. Тестируемость — одна из целей, но далеко не единственная.

На текущем проекте интеграционные за минуту всю систему проверяют, тут видимо от прямоты рук все зависит и от контекста проекта. Я не писал что против слоев (горизонтальных), DDD, вертикальных, хотя думаю что все это шляпа, надо что-то новенькое и более красивое )

ИМХО
Разделение на слои, микросервисные и любое другое разделение помогает потом когда делаешь фичу как оленю скакать между тонной файлов и проектов и добавлять по одной строке кода и еще тренирует память, чтобы нигде и ничего не забыть )

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

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

Для примера, сегодня написал два модульных теста, примерно по 100 строк каждый. Проверил позитивный и негативный сценарии. Запустил — вроде всё good. Затем написал интеграционный тест строк на 60 (проверяет туже функцию, только уже через контроллер), и вуаля — именно интеграционный тест отловил багу, поскольку как оказалось перепутал в рабочем коде идентификаторы сущностей.

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

Из «интеграционный поймал то, что юнит не мог поймать в принципе» не следует «юниты не нужны». Это ошибка выжившего. Юниты ловят баги в логике — тысячами за секунды. Интеграционные ловят баги на стыках — десятками за минуты. Замените одно другим — потеряете либо скорость, либо покрытие.

Я не утверждал что модульные не нужны, просто пирамиду тестирования многие делают не правильно, делая упор на модульные тесты, которые пишутся дольше, чаще требуют внимание. Модульные работают и оправданы там где нет внешнего API или других механизмов для проверки логики или большое количество вариантов (где интеграционный тест будет очень долго работать)

В моем понимании пирамида тестирования примерно такая

40% Модульные
40% Интеграционные
20% UI Тесты
+ Ручное тестирование для сложных кейсов, дизайна, где написание других видов тестов не рационально

Нет универсальной пирамиды тестирования. В моём текущем проекте на данный момент больше тысячи test-case'ов в юнит-тестах. Потому что в нём много внутренней логики расчётов, работы с данными, которые _проще_ покрыть юнит-тестами. Если у вас CRUD-джсоноукладка, то там юнит-тесты могут быть вообще не нужны (и часто даже мешают), а вот интеграционные вполне пригодятся. А если у вас расчёт отельных предложений на основании множества самых замысловатых критериев, то добро пожаловать в мир юнит-тестов. И пирамида тестирования в этих двух проектах будет очень разной.

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

«Функция больше экрана — повод задуматься» — хорошая маркер. Но, бывает и наоборот: функция в 10 строк, но класс с 15 зависимостями в конструкторе — функция формально короткая, а на деле God Object.
Но как первый фильтр — работает.

Я бы добавил 4-ю: на каждый чих выкидывать исключение вместо возвращения значения.

Например, если DI настроен нормально, то строки типа
_imageService = imageService ?? throw new ArgumentNullException(nameof(imageService));
не имеют смысла. Там всегда не null.

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

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

Если очень надо, а рефакторить такой код дорого, можно сделать PublicException, который будет показывать пользователю ошибку как есть. А всё остальное всё равно должно быть обработано. Желательно в одном месте (ExceptionHandlerMiddleware). Исключение должно сообщать о проблеме в коде/логике, а не быть способом передачи информации.

Тот же необработанный ArgumentException сообщает пользователю название параметра с неверными данными. Зачем это надо? (вопрос риторический)

Например, если DI настроен нормально, то строки типа _imageService = imageService ?? throw new ArgumentNullException(nameof(imageService)); не имеют смысла. Там всегда не null.

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

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

Про исключения vs Result-паттерн — тема холиварная. Мой подход: исключения — нормальный механизм, но они не должны перехватываться на каждом уровне. Бизнес-исключение (OrderNotFoundException, StatusNotAllowedException) должно долетать до единого обработчика (middleware), который формирует ответ клиенту. Если вместо этого на каждом слое try-catch с разной логикой — тогда да, проблема. Но это проблема не самих исключений, а их неправильной обработки.

Если я пишу какой-то сервис. для API, то я уже знаю о работе DI и что он мне вернёт. Нет смысла придумывать, что там что-то внезапно поменяется и из-за этого засорять конструкторы и тесты кодом, который никогда не будет выполнен и тестирует бесполезные вещи. Это может подойти для библиотеки, но не для класса внутри сервиса с известными параметрами работы. Иначе "на всякий случай" можно и код для .NET Core 3 версии написать, имея сервис на 10-й. Вдруг чего изменится.

Справедливо, для внутреннего сервиса это вопрос конвенции команды. Я привык ставить — дешёвая страховка, но понимаю аргумент про лишний шум.

Ещё добавлю про Result vs исключения: Result-паттерн хорошо работает в языках, где он встроен в систему типов — Rust, F#. Там компилятор заставляет обработать все варианты, и забыть не получится. В C# он добавляет церемонию: каждый вызывающий метод должен проверить result.IsSuccess, и если забыл — ошибка молча проглатывается. С исключениями наоборот — если не обработал, оно само долетит до middleware. В новом C# обещают discriminated unions, возможно с ними стоит пересмотреть подход.

Тут поддержу. Когда у тебя в методе один сервис возвращает "монаду", другой метод возвращает "монаду", но уже другую, то как-то грустно становится со всеми этими Optional. Несколько раз пытался перейти на этот паттерн и всякий раз бросал.

P.S. Правда это про общий случай. Почему в ваших примерах исключения вызывают у читателей вопросы я, в целом, понимаю.

>Тот же необработанный ArgumentException сообщает пользователю название параметра с неверными данными. Зачем это надо? (вопрос риторический)

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

Изменение схемы БД ломает бизнес-логику.

Так и должно быть, если трогались используемые поля. А если используемые поля не трогались, то ничего не сломается. Или я не пойму, вы хотите убрать используемые поля и получить запускающийся сервис? А что такой сервис вам насчитает?

Переименовали колонку ImageUrl в AvatarPath — бизнес-калькулятор рейтинга перестал компилироваться.

Во-первых, если вы уверены в том, что смысл не поменялся, переименуйте в модели, IDE вам всё переименует, всё скомпилируется.

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

И я не пойму, как вам интерфейс поможет. Ну вынесли вы ImageUrl в интерфейс. Ну поменяли в БД на AvatarPath и что? Ну теперь вам нужно всё то же самое, только ещё и интерфейс поменять.

Да, при переименовании IDE покажет все места — и это хорошо.

Но не все изменения в БД должны триггерить изменения в бизнес-логике. Простое переименование — не должно. Изменение типа — тоже не всегда. Например, ImageUrl была строкой-ссылкой, а стала коллекцией файлов с метаинформацией. Бизнес-правило «если у пользователя есть фото — добавить баллов» не изменилось. Но без абстракции калькулятор рейтинга ломается: вместо string.IsNullOrEmpty(user.ImageUrl) теперь нужно user.Images.Any(). С интерфейсом IUser калькулятор по-прежнему проверяет HasImage — и ему всё равно, строка там внутри или коллекция.

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

При чем тут Шарпей то? :)

Эту дичь ведь можно сотворить на любом языке.

У меня вот всё описанное имеется в проекте, но иначе не получалось. Всю эту опу прекрасно вижу и понимаю. Когда удастся выдохнуть проект ждёт мега рефакторинг. И это не Шарпей и не джава.

Эти антипаттерны не привязаны к языку программирования — они встречаются и в Java, и в Go, и в Python. Примеры покажу на C#/.NET, но суть та же для любого стека. Три конкретных случая из реальных проектов — и как их исправить.

Ошибка которую считают стандартом из-за удобства — DI. Конечно это не так плохо как ServiceLocator, но проблема в том что она делает много неявного и скрывает инициализацию. Явно лучше неявного, чем потом удивляться, почему в синглтоне вызывается Scope сервис.

>В идеале стоит пойти дальше — к полноценной ООП-модели, где логика расчёта рейтинга инкапсулирована в самом объекте User.

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

>Ввести доменную модель, которая ничего не знает о базе данных

Большая ложь. Если вы модель EF прикроете фиговым листочком интерфейса вы никуда не уйдёте от зависимости ваших моделей от фреймворков. Весь классический DDD намертво прибит гвоздями к используемой в проекте ORM, мнимая абстракция от персистентности - ложь, которая только запутывает дело.

Ага, рейтинг пользователя с точки зрения какого процесса

С точки зрения ограниченного контекста (Bounded Context). Рейтинг существует в конкретном контексте: репутация на форуме, кредитный скоринг, рейтинг продавца на маркетплейсе — это три разных рейтинга и три разных User. В каждом BC своя модель пользователя со своим набором поведения. Никто не предлагает лепить всё в одну «главную модель User» — это действительно был бы God Object. Но внутри своего контекста инкапсулировать логику рейтинга в User — нормально и правильно.

Весь классический DDD намертво прибит гвоздями к используемой в проекте ORM

Не согласен. Нормально написанный репозиторий отлично справляется в большинстве случаев: доменная модель не знает про DbContext, DbSet, навигационные свойства и lazy loading.

>Нормально написанный репозиторий

Так идите, объясните другим IT-евангелистам, что их любимый UnotOfWork - это не нормально написанный репозиторий. У них тоже свой единственно-правильный DDD.

>С точки зрения ограниченного контекста (Bounded Context). Рейтинг существует в конкретном контексте: репутация на форуме, кредитный скоринг, рейтинг продавца на маркетплейсе — это три разных рейтинга и три разных User.

А причём тут Bounded Context? Ваш пример (форум, банковский клиент, market-place) - это тот случай когда "пользователи" никак и никогда не будут связаны между собой. Нет, тут дело проще. У вас есть пользователь, в модели содержатся необходимые атрибуты для хранения этого пользователя в БД. Есть расчёт рейтинга пользователя, интересный для одной части системы. Этот расчёт может быть достаточно сложным и требовать не только базовых атрибутов пользователя, но и разных других связанных с ним сущностей. Возможно даже потребуется обращение к каким-то внешним системам, помимо самой БД. Кроме расчёт рейтинга пользователя возможен расчёт какой-нибудь "карты рекомендаций" для пользователя. Тоже непростая система с множеством алгоритмов. И всё это может жить в одной системе, в одном контексте. Как тут быть?

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

Так то и расчёт рейтинга, и расчёт карты рекомендаций - это отдельные сервисы, хоть в стиле ООП их оформляй, хоть в функциональном стиле. Как и было в вашем исходном примере. Но вы в качестве best practics зачем то предлагаете всё это в одном классе собрать...

>Смена ORM ломает бизнес-логику. Прямая работа с _dbContext.Add() и SaveChangesAsync() — это привязка к EF. Решили перейти на Dapper или вообще на нереляционную базу — переписываем метод целиком, хотя бизнес-правила не менялись.

Вы не сможете перейти на Dapper c EF, какими бы слоями абстракций не обмазывались. EF умеет в ChangeTrackker, Dapper - нет. Весь этот "бизнес-код", отделённый от "инфраструктуры" посредством "абстракции"-репозитория написан так, как написан, именно потому, что знает, что изменения в модели могут быть сохранены в персистентном слое, т.е., бизнес-слой неявно много знает о том, что стоит за IOrderRepository и за его SaveChanges().

Можно, конечно, пойти другим путём, как вы примерно и переделали пример. Не использовать паттерны вида UnitOfWork (SaveChanges()), сделать операции сохранения более гранулярными. Но это означает:
1. Ради абстракции лишать себя удобства, которые предоставляют фреймворки.
2. ChangeTracker для Dapper вы в рукопашку всё равно не забабахаете, поэтому ваша имплементация неизбежно будет пытаться сохранить агрегат целиком, вместо того, чтобы одну строчку в табличку добавить. Со всеми вытекающими проблемами производительности и конфликтов изменений.
3. Опять же приходим к тому, что интерфейс репозитория диктуется не одной только бизнес логикой (сверху-вниз), но и соображениями о персистентности. Если вас (зря) волнует перспектива перехода с EF на Dapper или на произвольную NoSQL, то и в интерфейс вы должны закладывать возможность простой имплементации на любом из этих движков. Это намного труднее, чем просто выбрать один framework.

Вы не сможете перейти на Dapper c EF, какими бы слоями абстракций не обмазывались. EF умеет в ChangeTrackker, Dapper - нет. Весь этот “бизнес-код”, отделённый от “инфраструктуры” посредством “абстракции”-репозитория написан так, как написан, именно потому, что знает, что изменения в модели могут быть сохранены в персистентном слое, т.е., бизнес-слой неявно много знает о том, что стоит за IOrderRepository и за его SaveChanges().

Не соглашусь насчёт ChangeTracker. Хорошо написанный репозиторий эту проблему решает: окно работы с трекером сводится к минимуму — получили агрегат, изменили, репозиторий внутри себя дёрнул SaveChanges() и вернул управление. Снаружи бизнес-код не знает ни про трекер, ни про SaveChangesAsync.

Если работать с одним агрегатом за транзакцию (что в DDD и так считается нормой), то EF меняется на Dapper без драмы: в реализации репозитория меняется способ загрузки/сохранения, а интерфейс и доменный код — нет. Я так пишу в своих проектах, и миграция слоя персистентности — это действительно переписать репозитории, а не весь проект.

Я вам, как-будто, подробно разъяснил по поводу возникающих проблем, а вы мне отвечаете, что "проблем нет". Я в джсоноукладке много лет проработал, на меня оптимизм IT-евангилистов с их "хорошо написанными репозиториями" мало впечатления производит.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации