Pull to refresh
4
Send message

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

Что ж, называйте DTO всё, что вашей душе угодно, не буду спорить.

Не знаю, может вам и получится найти источник, в котором говорится, что предназначение DTO - передача данных между слоями приложения, но в том источнике, на который вы сослались, говорится следующее: "The application service is then responsible for moving data back and forth between the DTOs and the domain objects". Т.е., ровно о том, про что я писал выше.

>Выражение “data transfer” означает передача данных, это и есть определение. Передавать можно и между разными слоями приложения.

В программировании всё везде передаётся. Параметры передаются в методы, результаты передаются обратно, между "слоями" приложения тоже можно параметры передавать, хотя физически в приложении никаких слоёв нет, это условность и абстракция. Всё можно назвать "data transfer", поэтому выделять некие объекты в DTO на основании того, что они куда-то "передаются" и это их "определение" просто бессмысленно. Любой параметр на этом основании можно было бы назвать DTO.

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

Ну, понятно, что с оплату заказа надо делать в одной транзакции, если всё хозяйство в рамках одной БД находится. И это совсем не сложно, если просто делать, а не выяснять какой агрегат на ком стоит.

DTO между подсистемами данные передаёт. Между фронтовым клиентом и бэком. Между двумя сервисами по шине данных. Между бэком и хранимкой (и такое может быть). Поэтому DTO сильно привязан к вопросам инфраструктуры. DTO в REST API должен быть написан таким образом, чтобы с ним было удобно работать REST-клиенту, чтобы его было удобно сериализовать и валидировать. К DTO, привязанным к другим источникам инфраструктуры, эти источники будут диктовать другие требования. Я долгое время работал с системой, внешний API которой был сделан в виде хранимок БД, которые возвращали документы XML. Соотвестственно и классы DTO должны были учитывать всю эту специфику XML, которой совсем не мало.

А маппинг из DTO в доменнные объекты - это как раз ответственность слоя приложения, чтобы DTO между слоями не гонять. Делать доменные модели осведомлёнными о DTO или не делать - это, конечно, дело вкуса, но никакого "по определению" тут нет.

>Потому что DDD ставит задачу написать бизнес-логику так, чтобы её мог прочитать и проверить не программист, а бизнес-заказчик.

Во, вот это на мой взгляд ключевое! А очень люблю книгу Эванса, особенно в её soft-части, но вот эта его идея про "код, как книга" и "чтение кода бизнесом" повела куда-то не туда. А последователи-евангелисты, вместо того, чтобы этот тезис скорректировать, только всё усугубили.

>идеальный способ реализации: загрузить все документы из базы в класс Documents, вызвать на нём метод add, который провалидирует, возможно ли это добавление, по всем необходимым бизнес-правилам, в том числе и проверит уникальность

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

Записью в табличку TransactionalOutbox можно сделать. Ваш предложенный код всё равно страдает проблемой, что сущность может создаться, а письмо не будет отправлено. Если при отправке письма возникнет исключение, тогда либо пользователь получит ошибку, при том, что нужная сущность была создана и пользователь к своему удивлению обнаружит её при очередном обновлении списка. Если же перехватить исключение, то письмо не будет отправлено, а пользователь получит 200 при неполностью выполненном запросе.

Но с вашим основным посылом я согласен. Перетаскивать всю бизнес-логику на уровень моделей - это делать модели очень сложными. Модель и так должна отвечать: 1) за своё согласованное состояние, 2) за персистентность (как бы это ни отрицали сторонники "чистого" DDD). Навешивать сюда ещё взаимодействие с внешними сервисами... Ну, такое.

P.S. Сторонники "чистого" DDD вспомнят в этом случае о доменных событиях, но это просто новый слой вопросов. Если у вас в требаниях была логика "после A должно быть B и C", то в коде сервиса это будет простой линейной историей, а в коде на основе событий это превратится в винегрет.

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

Но всё равно получается громоздко. Тут проявляется одна из основных проблем DDD, о которой не много говорят, что DDD плохо скрещивается с инфраструктурой.

На инфраструктурном уровне у нас есть прекрасная библиотека валидации - FluentValidation, которая может в хорошо форматированном виде рассказать нам о проблемах входной DTO. C DDD нам либо:
1. Не использовать Fluent, а использовать фабрику, способную вернуть "доменную" ошибку валидации, а потом маппить эту ошибку на поля DTO, чтобы вернуть в ответе REST. Жутко трудозатратно.
2. Внедрить в доменную фабрику IFluentValidation, но тогда доменная фабрика будет знать о "внешнем" по отношению к домену слое DTO.
3. Так или иначе - дублировать логику. Полноценно использовать FV на уровне API, в доменке дублировать проверки и кидать простые исключения в случае ошибок (при правильном pipeline данных до этих исключений всё равно не дойдёт). Ну, или забить на такие проверки на уровне домена, полагаться на проверку на уровне Приложения.

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

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

Вообще, на мой взгляд главная проблема DDD в фетише отделения доменного кода от инфраструктуры. Для валидации DTO есть такая удобная вещь, как FluentValidation, для работы с БД такая удобная штука, как EF. Но FV с проверкой доменной модели скрещивается плохо, потому что либо доменная модель должна знать о DTO, чтобы использовать Fluent, либо проверки на уровне доменки и API должны быть продублированы. Либо полагаться на входную валидацию и считать, что к нам приходят "правильные" DTO (одной лишь структурной валидации для проверки валидности ведь мало).

С персистентным слоем + ORM та же проблема. Ты персистентность из моделей в дверь, а она влетает в окно. Времена, когда EF диктовал, что "доменная модель" - это набор get/set-свойств, давно прошли, современный EF куче всего научился. Но уши персистентности всё равно то там, то тут торчат. Не десериализуешь ты сложную модель нормально без доступа к приватным полям через рефлексию или без специального конструктора.

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

>в конструктор надо передавать DTO со всеми этими полями

В конструтор чего? Доменной модели? А как же независимость доменной модели от слоя приложения/представления?

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

По WhatsApp уже несколько месяцев с заграницей разговариваю.

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

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

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

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

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

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

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

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

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

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

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

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

1
23 ...

Information

Rating
5,015-th
Registered
Activity