Как стать автором
Обновить

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

А как всё это согласуется с SOLID? особенно с S и O?

class Order из примера:
отвечает и за совместимость продуктов и за расчёт налогов, не очень то single
содержит IsValid — который будет регулярно меняться, конфликтует с closed
Валидатор и прочие части могут быть представлены отдельными классами, но все равно именно агрегат-рут должен гарантировать соблюдение бизнес-инвариантов в целом.

OCP не применим для уровня domain, который по сути своей хардкод бизнес-логики. Entities вообще всегда final.

Понимаю, что не вы автор, но там много спорных вопросов.
Этот класс моделирует и данные, и логику

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

и даже содержит логику, определяющую, может ли клиент совершить покупку

Rich Domain Model должна быть в ограниченном контексте. Именно это позволяет максимально соблюдать SRP.

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

Ну если бы не было вопросов, то и… вопросов не было бы :)


Там есть два очень важных момента:


  1. Антипаттернов — нет. Есть конкретные задачи и условия, и только от них зависит, что и как использовать.
  2. Так назваемые "анти-паттерны" сразу откидываются, не задумывась насколько они применимы. Я это замечаю все чаще и чаще :(
Альтернативная точка зрения.

Из прочитанного выходит что данные надо хранить вместе с их обработчиками, слой бизнес логики «зашит» в объекты-агрегаты. А ведь одни и те же данные могут использоваться/обрабатываться в разных ситуациях. Получается при разрастании функционала нужно добавлять новые публичные методы в объекты-агрегаты (нарушение принципа открытости/закрытости)? Или не правильно понял.

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

При использовании универсальных моделей, без выделения агрегатов, действительно, связанность имеет тенденцию увеличиваться. (Пример: когда entity framework объекты используются как доменные объекты, свои плюсы тут тоже есть).

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

Может кому пригодится: на тему связанности и слоев хорошая книга «Внедрение зависимостей в .NET, Симан Марк».
Из прочитанного выходит что данные надо хранить вместе с их обработчиками, слой бизнес логики «зашит» в объекты-агрегаты. А ведь одни и те же данные могут использоваться/обрабатываться в разных ситуациях. Получается при разрастании функционала нужно добавлять новые публичные методы в объекты-агрегаты (нарушение принципа открытости/закрытости)? Или не правильно понял.


Тоесть в случае анемичной архитектуры — при добавлении публичных методов в сервисы, open/close нарушен не будет?

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

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

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

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

В статья говорится
Используя агрегаты мы пишем бизнес-логику в одном месте

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

Кажется логичным:
  • что данные от представления лучше хранить отдельно
  • что работу с данными и сами данные лучше разделять

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


Думаю в тех кейсах где Вы добавите новый сервис, я смогу добавить новый агрегат. Но, что если речь идет о каком нибудь OrderService и вам нужно имплементировать фичу, ну например отмены заказа. Будете ли вы создавать отдельный сервис для этого? Ведь сейчас к модулю Order у вас есть четкий API находящийся в OrderService, какой смысл разделять его? Я считаю, что в данном случае никакого, ровно та же аналогия и с агрегатами которые также предоставляют API к доменному слою.

что работу с данными и сами данные лучше разделять


В случае ООП это противоречит инкапсуляции.

В статья говорится
Используя агрегаты мы пишем бизнес-логику в одном месте


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

Как бы мы не старались, не всегда возможна полная чистота и часть логики мы делегируем Доменным Сервисам. Но этот слой более худой.
Ведь сейчас к модулю Order у вас есть четкий API находящийся в OrderService, какой смысл разделять его? Я считаю, что в данном случае никакого, ровно та же аналогия и с агрегатами которые также предоставляют API к доменному слою

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

Т.е.
— в описанном в статье подходе:
добавление нового сервиса — нарушение концепции.
— а при подходе с отдельными бизнес сервисами:
добавление нового метода в сервис — не нарушает концепции.

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

Думаю в тех кейсах где Вы добавите новый сервис, я смогу добавить новый агрегат

Пример был бы полезен. Разве объект-агрегат для одной бизнес сущности (например в статье «Заказ») не должен быть один?

В случае ООП это противоречит инкапсуляции

Не понял. Почему код обрабатывающий данные, вынесенный в отдельный от данных класс, нарушает инкапсуляцию.
Разве объект-агрегат для одной бизнес сущности (например в статье «Заказ») не должен быть один?


Если мы говорим про большую систему, то Заказов и агрегатов где они участвуют (и не всегда являются корнем) будет несколько.

Entity у вас описывает таблицу БД и отображает ее данные, я правильно понимаю? То есть, это Entity из ORM. А агрегат - это корневая Entity. Если так, то при создании нескольких агрегатов, у вас будет несколько таблиц в БД. А если вам нужна одна таблица, а логика может быть разной для этих данных, например, создание заказа на сайте и в панели управления? Или в одной таблице хранятся разного рода данные, например сопособ доставки, оплаты, статус заказа, корзина, купон. И всю эту логику вы будете писать в одном классе? И каждый раз нужно будет добавлять новую логику в этот ограмный класс. Это противоречит принципам единой ответственности и открытости-закрытости. Или для каждого столбца, грубо говоря, создадите таблицу? Ну бред же. Так же вам придется передавать много зависимостей, и здесь вы не сможете использовать DI-контейнер. Это шаг назад в разработке на 20 лет.

Этот принцип с агрегатами вы можете применить немного по-другому. Агрегат - это сервис, Entity - это сервис, а сущность из ORM - это простой объект, DTO или структура данных. И эти данные вы передаете в Агрегаты/Ентити сервисы для обработки. Получится то же самое, только без перечисленных выше проблем. Но запись будет немного отличаться, вместо order->calculate(...куча зависимостей) будет orderAggregate->calculate(order).

  1. Доменная модель не знает как она будет храниться. Некоторые авторы называют идеальным хранилищем документоориентированные бд. Так что абзац про орм немного не про то что написано.

  2. В таком случае orderAggregate становится тем самым сервисом от которого я призываю отказаться. Ну и кучу зависимостей в метод Calculate при правильной нарезке обычно не придется передавать.

Получается при разрастании функционала нужно добавлять новые публичные методы в объекты-агрегаты (нарушение принципа открытости/закрытости)? Или не правильно понял.


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

Потому что данные начинают использоваться в разных сервисах. И у сервисов есть свои предположения насчет друг друга. Сервис Б, например, ждет, что сервис А установит поля X, Y, Z.
Согласен, есть такое (можно с этим бороться). В вашем подходе такая проблема тоже может возникнуть: у верхних объектов есть предположения насчет нижестоящих и даже наоборот может быть.

Интересно сколько объектов (в среднем), помимо самих данных, передается в конструктор объекта-агрегата в вашей реальной системе. Кроме самих данных у них должны быть зависимости от сервисов. Например, расчет цены может быть разным для разных клиентов, надо разные реализации расчета инъектировать. Налоги надо считать и т.п.
Есть мнение, если объект имеет более 4-х зависимостей (стремиться к God object), то имеет смысл подумать о декомпозиции.

Интересно, в вашем подходе в реальной системе, возникают ли такие случаи и как тогда производить декомпозицию?

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

Бывают менее чистые, например, если у нас требование уникальности телефона, email и т.п. В агрегат не засунешь базу пользователей и надо куда-то ходить.

У Владимира в статье хорошо расписано.
НЛО прилетело и опубликовало эту надпись здесь
Если я правильно понял — анимичная модель это шаг в сторону Процедурного программирования. Возможно стоит рассмотреть в отдельной статье вопрос: в каких случаях Процедурное программирование предпочтительнее ООП?
Да, анемичную модель часто считают процедурной (или даже функциональной, если она является иммутабельной). У не знаю когда ООП хуже процедурного стиля. Наверное есть применения, но кажется, что реальный мир проще и качественней проектируется посредством ООП.
В сфере разработки игр, лично я предпочитаю процедурный стиль (паттерн ECS) стилю ООП (компонентный подход Unity). ECS куда гибче, и как это не странно, с этим паттерном получается построить куда более строгую архитектуру, несмотря на ослабление инкапсуляции и глобальные данные (компоненты).
Я как-то зашорился на бизнес-приложениях. Конечно же, за пределами есть свои подходы и решения.
Добырй вечер. У меня по теме статьи есть несколько вопросов. Они, вообще-то, общего характера, но для наглядности постараюсь привязать их к примеру примере из статьи.
Вопрос первый (касается разделения сущностей). При обработке заказа клиенту нередко может быть сделана скидка. Где в этой модели разместить реализацию ее бизнес-логики — учитывая, что скидка может зависеть и от клиента, и от состава и объема заказа, и от канала продаж, и от дня заказа, и ещё от чего-нибудь? Далее, если рассчет скидки — ответственность объекта Order, и при этом скидка делается по определенным правилам, то возникает вопрос, какой компонент бизнес-логики реализует эти правила, и в частности — хранит их?
Вопрос второй (касается разделения контекстов). Допустим, в предметной области в некий момент существовали два канала продаж (скажем, розничные продажи и продажи по заказам) каждый — со своими процессами. Следует ли их реализовывать как разные контексты с разными объектами? Или же их следует обрабатывать в одном контексте, с единым набором объектов? Или же реализовать промежуточный вариант: сделать часть объектов универсальными, испольуемыми в нескольких контекстах? Ведь каждый вариант имеет свои затруднения. К примеру, если для разных каналов продаж используются разные контексты, то что делать с общей для них общая бизнес-логикой (например, если часть правил предоставления скидок едина): дублировать ее в разные контексты, или ещё какой вариант? И как быть, если после оптимизации бизнес-процессов окажется, что оба канала продаж начнут использовать один и тот же набор бизнес-процессов, т.е. разделение на контекстов пропадет. Что делать с разными наборами объектов тогда? Ну, а если использовать универсальные объекты, то не будут ли реализации бизнес-логики разных контекстов чрезмерно связанными друг с другом (при том, что в дествительности такой связи нет)? Такая связь ведь очевидным образом усложнит модификацию программы при изменении процессов (и связанных с этим изменения бизнес-логики) только в одном из каналов продаж, когда объекты перестанут быть универсальными.
PS Как мне видится, эти, непростые для подхода DDD, вопросы достаточно легко решаются в рамках подхода «анемичной» модели данных. Может, все-таки не следует считать ее «антипаттерном»?
Начну с конца. В анемичной универсальной модели такое решается кучей ифчиков и предположений. В итоге получаем BBoM.
Попробую разложить предметную область, но надо больше времени.
Даже не знаю, стоит ли раскладывать предметную область. Основную мысль я, кажется, и так донес — что «серебряной пули нет» ((с)Ф.Брукс), что у разных подходов есть и достоинства, и недостатки, и в разных обстоятельствах оптимальными будут разные подходы (если бы ещё заранее знать, какой именно в этом конкретном случае -совсем бы хорошо было).

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

Основное отличие ООП от процедурного подхода это наличие явной модели в коде.

Это все так, но «анемичная» модель — это не обязательно именно процедурный подход. В ней логика обработки данных совсем не обязательно должна быть реализована на процедурах без состояния. Вполне возможна и реализация с испольхованием ООП. Например — по шаблону проектирования flyweight, т.е. — с разделением состояния на внешнее, инкапсулированное в объектах данных (оно обычно долгоживущее и устойчивое — короче, полный ACID) и внутреннего, краткоживущего, с временем жизни на период выполнения операции — в объектах операций.
PS Кстати, не исключаю, что сторонник DDD, знающий его по-настоящему глубоко, скажет, что то, что написано выше — это «на само деле» есть вариант DDD. Охотно поверю — ибо в объективной действительности четких и резких граней нет: четкие и резкие грани — они в нашем сознании.

Как в этом подходе будет выглядет операция бизнес-логики, вовлекающая в себя 16 агрегатов?

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

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

Мы зачастую преувеличиваем важность strong consistency: бизнес живет в парадигме eventual consistency.

А это здесь при чём?

НЛО прилетело и опубликовало эту надпись здесь

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

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

Правильно ли я понял что за доменную сущность заказа и доменную сущность товара должны отвечать разные команды?

Могут, я бы сказал. Зависит от размера и количества команд.

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

То есть положим у нас средней руки CRM, которая работает с товарами, заказами, поставками, налогами и стоком (учётом товаров на складах). Сколько человек будут такую разрабатывать если на каждый агрегат отдельная команда? Ну так… навскидку.

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

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

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

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

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


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

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

Анемичная модель так же не запрещает сделать вам ограниченный контекст.

Ровно тот случай когда мы хотим банан, а получаем гориллу и джунгли в придачу. Есть 10 кейсов с ордером, в каждом из них нужно 20% функций ордера, но в каждый из них мы тащим большой и сложный ордер. Например есть кейсы где таксы не нужны, а в ордере они есть. Есть кейсы где у ордера просто меняется статус, но мы тащим ордер, в котором еще и валидация цен и позиций имеется. И так далее и тому подобное. Вы об этом говорите в разделе про универсальную модель, но не говорите как этого избежать. И чем отличается ваш финальный Order, от "плохого" Order из раздела про универсальную модель?


Что вы делаете с функциями, которые с одинаковой силой тяготеют к двум и более агрегатам?


Что вы делаете с похожими аггрегатами, типа ордер для юзера или ордер для админа. Ордер админа поддерживает часть функций ордера юзера, паручку своих, но ордера админа еще и показываются списком по 100 штук за раз.


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


Вы не должны повторять себя именно в поведении, и не бойтесь повторять себя в данных.

Кажется поведение и данные обычно сильно связаны. Как этот принцип должен работать?


        //Существует ли более одного call site, которое вызывает этот метод? 
        //А если нет, то почему бы код не заинлайнить в этот call site? К тому-же, там уже есть коннект к базе, ведь product он как-то достал.
    public void AddProduct(Product product)
    {
        // А если тут нужно в базу сходить?
        if (!IsValid(product))
        {
            return;
        }
        _items.Add(product);
        // что если добавление продукта должно добавить новый налог, как этот налог из базы достать?
        RecalculateTaxesAndTotalPrice();
    }

Как это все в базу сохранять?

Пятиминутка саморекламы: если вам всё ещё хочется банан — посмотрите Tecture. :)

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

НЛО прилетело и опубликовало эту надпись здесь
Всегда можно заиспользовать инструменты соего языка, чтобы избежать физического дублирования

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


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

Ордер в админке, у клиента и в репортинге. Все не эквивалентны, но пересекаются по функциям. Встречается в 100% систем с ордерами. Как проводить границы контекстов?

Я не вижу ничего плохого в том, чтобы в нескольких контекстах иметь одинаковые функции.

В этой статье как раз недавно было плохое: была общая, скажем для простоты, функция, возвращавшая список товаров для двух групп задач из разных контекстов: складской учет и товары для заказа. В процессе развития системы выяснилось, что требования к функции для этих разных групп реально сопадать перестали (ЕМНИП, для складского учета там нужны все товары, а к заказу доступны не все, а только с определенным признаком). Плохое же оказалось в том, что мест, где запрашивается эта функция — много, и для такого изменения нужно аккуратно прогуляться по всему коду и поправить там (и только там), где нужно.
чем отличается ваш финальный Order, от «плохого» Order


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

Модель без базы это очень специфическая вещь. Ее можно сделать, у нас в бирже так и есть, но все усложняется многократно. Если не брать такие ниши, то база или иное IO в модели нужна всегда. А значит в наш прекрасный order попадёт штука вроде IDbConnection. А как только мы такой ордер в админке захотим показать, мы выберем из базы 100 таких объектов и в каждый DI заботливо вставит коннект к базе. В какой-то момент это станет напрягать и для админки появится свой агрегат — но беда, часть функций дублируется между админкой и клиентским ордером. Создаётся слой сервисов для ребра кода, в терминах которого реализуется клиентский и админский ордер. Этот слой оперирует уже не агрегатами, а тем что вы из базы достали, тупыми анемичными моделями. В итоге вы поверх анемичной модели сделали надстройку ради красивого синтаксиса в контроллерах. Где моя цепочка рассуждений рвётся?

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

БД одна, это же ордера и их не миллиарды, кроме того обсуждаем админку, которая клиентские ордера показывает.


Операции клиентского и админского ордера не все одинаковые, а процентов 50, вещи вроде отменить заказ или добавить в заказ что-то или полученные бонусы посмотреть. Так что «загружать агрегат» не выйдет, в агрегате клиента только 50% нужного функционала есть. Так что нужно как-то все реюзать и не скатиться к квази-DDD поверх анемичной модели :)


Один кусочек анемичной модели у вас уже есть — вы список ордеров для админки достаёте не в виде списка «DDDOrder», а неких анемичных «OrderRecord» потому что так а) быстрее б) удобнее. Этот OrderRecord затем превращается в OrderListItem и уже уходит на клиента.


Нельзя не заметить, что OrderRecord удобно использовать внутри AdminDDDOrder, потому что 100% полей OrderRecord там дублируется. Это будет ещё один шаг к анемичной модели. Затем реюз части функций ClientDDDOrder — код уезжает в сервис или утилиту и этот код оперирует OrderRecord так как это тупо данные из базы и они есть и там и там в одинаковом виде. Затем заметим, что 50% кода в сервисах и работают с OrderRecord, а остальные в AdminDDDOrder и ClientDDDOrder — желательно ввести единообразие и весь код уезжает в сервисы. И вот у нас сверкающий DDD внутри которого старая добрая анемичная модель. Может ли бать по другому?

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

Второе, ОрдерРекорд — не анемичная модель. Это по сути readonly модель без логики и сеттеров.
Во-первых, обойдемся без деления на админскую часть и клиентскую.

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


Если вы хотите разделить эти агрегаты, то скорее всего придется разделить и хранение. И перейти к eventual consistency.

Ну уж нет, это, мягко говоря, перебор. По этой логике выходит, что пока бухгалтер с поваром пересекаются по простым данным, которые в базе лежат — все хорошо. А как только бухгалтер с поваром решили, что им нужна пара одинаковых функций так сразу у нас 2 БД и eventual consistency.


Второе, ОрдерРекорд — не анемичная модель. Это по сути readonly модель без логики и сеттеров.

Не read-only, она же возникает как ответ на желание повторно использовать логику в админке, у клиента и в прочих местах. Будет же что-то вроде


class OrderService
    void ApplyPromo(OrderRecord order, string promo);

class AdminOrder
    OrderService OrderService;

    void ApplyPromo(...)
        OrderService.ApplyPromo(...);

class ClientOrder
    OrderService OrderService;

    void ApplyPromo(...)
        OrderService.ApplyPromo(...);

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

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

Это если вы хотите построить универсальную модель. В статье я отговариваю от этого и считаю анти-паттерном.

Ну а все дальнейшие размышления идут насчет того как скрестить DDD и универсальную модель.

В идеале никак, но есть большая часть DDD про contexts map – там все расписывается, в том числе и shared kernel.
>>Каждая из этих областей будет в обязательном порядке использовать какую-то часть функций из клиентского ордера.
>Это если вы хотите построить универсальную модель. В статье я отговариваю от этого и считаю анти-паттерном.

«Каждая из этих областей будет в обязательном порядке использовать какую-то часть функций из клиентского ордера» — это не мой произвольный выбор как проектировщика, это просто законы бизнеса, на которые я повлиять не могу. Я не хочу универсальную модель, хочу красивые агрегаты как в статье, но не понимаю как это вообще возможно. Я же не с потолка беру требования — админка в которой админ может выполнить часть функций клиента это не редкость, репортинг который шарит какие-то расчеты с клиентом тоже несложно представить.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Я не о модели БД говорю, а об агрегатах, посмотрите выше кусочек кода. Вещи вроде «IsValid(product)» запросто могут требовать (и чаще всего требуют) сходить в базу.

Давайте на конкретном примере, может?


IsValid(product) вообще выглядит некорректно, напоминает валидацию анемичной модели, свойствам которой может быть присвоено что угодно снаружи.

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

IsValid(product) взят из статьи и в ней он показан как пример «хорошего» кода, а вы говорите, что он «плохой»… Напоминает войны про «правильное» и «неправильное» ООП лет 10 назад. Тогда каждый автор рассказывал примерно такую историю — есть много людей которые неверно понимают ООП и пишут процедурный код в ООП стиле. А есть рецепт правильного ООП, которые известен автору, потому что он собаку съел на этом. Это продолжалось до тех пор пока все не переключились на ФП. Не разобравшись как готовить ФП далее все перекинулись на DDD.
Приватный IsValid добавлен с целью показать, что агрегат может отказаться добавлять что-то в себя, если посчитает такой продукт невалидным для своего состояния, нарушающим его инвариант.

А. В статье плохой нейминг, этот метод должен называться иначе, что-то типа canAddProduct. И, конечно, он может быть только приватным.


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

Это не задача агрегата "заказ".
Как вариант, агрегат "заказ" может принимать аргументом обертку над продуктом, в которой уже содержится информация о доступности.

Соглашусь, наверное.

Поправил в примере на вариант CanAddProduct.

Напомню, мы обсуждаем IO в модели и довольно простое утверждение — « база или иное IO в модели нужна всегда.» То, что для частного случая можно придумать какие-то варианты не очень интересно. Либо мы с вами говорим о том, что делать с тем, что в модель (она же «умная») постоянно лезут всякие db connection, либо обсуждаем некий универсальный подход, который позволяет любого IO всегда избегать.

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


Главную ценность DDD я вижу в том, что бизнес-логика четко выделена и фактически самодокументирована кодом domain слоя. Это очень выгодно отличается от большинства проектов, которые мне попадались в жизни, — когда по коду достоверно восстановить чистую бизнес-логику практически нереально, и приходится выпытывать детали у тех, кто еще что-то помнит (и обычно помнят не очень). А технические паттерны это детали, можно и на ActiveRecord-ах сделать DDD, просто неудобно.

Судя по вашему ответу у нас такая ситуация
Либо мы с вами говорим о том, что делать с тем, что в модель (она же «умная») постоянно лезут всякие db connection, либо обсуждаем некий универсальный подход, который позволяет любого IO всегда избегать.


Давайте обсуждать. Мне бы очень хотелось знать как бизнес-логика вроде «если не удалось сконвертировать EUR в RUR по любой причине, или результат конвертации сильно отличается от заявленной цены товара, то товар в корзину добавить нельзя, а сам товар помечается как недоступный» оказывается внутри модели, но при этом модель IO не делает.
НЛО прилетело и опубликовало эту надпись здесь

Самый банальный способ — это double dispatch, когда на уровне domain объявляется интерфейс, а в метод передается реализация, которая уже может делать i/o.


Часто можно сделать элегантнее, изменив порядок вычислений. Простейший пример (хоть и не совсем про i/o) — в модели пользователя вместо метода updatePassword(plaintextPassword, hashCalculator) сделать метод updatePasswordHash(passwordHash).


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

Типа такого?
void AddProduct(Product p)
{
    _io.DoSomething(p);
}

против
void AddProduct(Product p, IO io)
{
    io.DoSomething(p);
}


Только теперь вам нужно таскать везде OrderRepository и IO, а если у вас много методов в модели, то может быть IO1, IO2,… ION. Это просто напросто неудобно. Кроме того, когда клиент Order «замусоривается» этими IO1..ION теряется тот самый ubiquitous language, о котором в DDD так много говориться.

В анемичном случае у вас будет OrderService, а внутри него будут жить IO1, IO2,… ION и клиенты этого OrderService ничего про эти детали знать не будут. Это удобнее.

Да, поэтому double dispatch и стараются не использовать.


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


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


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

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

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

Тут DDD вообще плохо подходит — DDD максимально конкретизирует, хардкодит бизнес-логику, а тут задача стоит ровно обратная.

Я бы сказал, что DDD подталкивает к построению DSL на языках максимально для этого не предназначенных. В языках где с DSL все хорошо и с объектами (и без них) все замечательно. Тот же Ruby, в котором максимально приятно писать бизнес логику.

а почему в модель должны лезть всякие db connection? если даже предположить, что в модели нужны IO, то в модель попадает интерфейс, например, репозитория, в реализации которого и есть всякие db connection. и при желании сменить db connection на nosql db connection (или все, что угодно) никаких изменений в модель вносить не придется.

тут не об этом речь, посмотрите выше по треду, должно быть понятно

Я тоже сильно путаюсь в этих агрегатах и у меня такой вопрос: если к примеру сущность Tax является частью агрегата Order, то может ли существовать отдельный репозиторий TaxRepository для получения объектов Tax?

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

на самом деле не все разделяют это мнение
Допустима ленивая загрузка. Подробнее в статье.
Спасибо огромное, хорошая статья!
Приходилось ли кому натягивать сову на глобус попробовать имплементировать DDD для задач где есть работа с календарем?

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

Можно смотреть на это вещи и из состояние. Допустим есть у нас:

Item {
id
title

}

state block {
id
startDate
endDate
itemId
status [available, pending, reserved, unavailable]
userId
}

У нас вот задача есть похожая но на порядок сложнее (для примера я ооочень сильно упростил), и мы вот думаем как впихнуть это в агрегат. И что-то сомневаемся…

1. Много конкурентных запросов и команд для определенной вещи от разных людей, апи и скедулеров которые в конце концов манипулируют стейтом. Создают, меняют даты, статус, овнеров и т.д.
2. Стейты могут накладывать друг на друга с разными датами, статусами и т.д по определенным правилам.
3. Нужно обеспечить целостность данных. Например 2 запроса одновременно могут увидеть что вещь свободна (!reserved, unavailable), и попытаяются зарезервировать его. В это время кто то может вручную заассайнить вещь на эти же даты на другого человека. И в это же время какой нибудь скедулер отменяет стейт в том же диапазоне дат потому что статус pending ничего не решает.

Вот что мы думаем.

1. Стейт блок как агрегат. Но, пользователи работают с датами календаря и вещами. Они не видят и не знают что это имплементировано в базе стейтами с айдишниками. Айдишники это скорее для девелоперов просто что бы оперировать данными в базе. Пользователи могут выбрать вещь и отменить пользование в определенных датах, а внутри уже наш код знает как обрезать стейты, делаить их или мержить. Их может быть несколько эти стейт блоков. Не вариант вобщем.

2. Item это агрегат рут. И вся работа со стейтами (календарем) будет идти через него. И так как он должен быть под транзакцией и никакого конкурентного доступа к нему не должно быть на запись по определению агрегата (если я не ошибаюсь), мы можем загрузить в память календарь этого агрегата по itemId, делать с ним что хотим, со всеми этими стейтами, пока остальные ждут, и потом сохранить. Чтобы не было конкурентного доступа на мутации будет кафка с партишенами по айтему ну и/или оптимистик лок и/или транзакции. НО — если человек хочет взять вещь на след неделе, на 4 дня, то его запрос будет ждать пока агрегат освободиться, потому что кто то зарезервировал пользование на сегодня и идет обработка. Ну то есть мы убиваем конкаренси тут конкретно, а нам она нужна.

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

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

Вот пытаемся на sqrc + возможно event sourcing + materialized view и все это в рамках DDD запилить. Что бы и история была, вьюшки под разные запросы разные создавались, и что бы избавиться от проблем невалидных данных в базе. Только вот положить этот стейт на агрегат не можем. Грузить например 2 года календаря в память что бы изменить пару дней как-то некошерно. Можно поиграться и загрузить только релевантные данные в память если получиться (например стейты ближних дат для проверки
конфликтов и другие валидации) + по itemId, и после мутации сохранить опять в базу. Но опять же, мутаций много. Некоторе из них нужно выполнить очень быстро (может приоритет запросов на мутацию тут поможет).

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

Вобщем мозг взрывается =)) Может кто чего подскажет почитать, посмотреть.

З.Ы.
Может все таки у нас нет выбора кроме как грузить в память состояние календаря, но попытаться делать все так быстро что бы по максимому обеспечить throughput и компенсировать недостаточную конкурентность/параллельную обработку?
НЛО прилетело и опубликовало эту надпись здесь
Конкурентные запросы упрутся в транзакции, кто первый запрос бросил — того и тапки.


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

Ну у нас такие доводы в пользу кафки (сейчас используем раббит и в основном без транзакций, потому что из еще не было в монге когда это разрабатывалось):

1. Пользователи насилуют систему как хотят и нам надоело иметь дело с параллельным процессингом или вообще обратным порядком команд. Например когда они делают Update -> Delete. На самом деле они могут делать много операций и нам это делает проблемы. С раббитом мы теряем order потому что скейлинг.
2. Кафка обеспечит нам partial order (партишенинг кафки по агрегату)
3. Опять же из-за партишенинга, у нас есть возможность использовать locality data patterns и работать с агрегатом при помощи load data -> mutate -> store, и не бояться что в соседнем воркере хэндлер тоже что-то сделал с этими данными.
4. Ну и в связке с DDD хотим топики и т.д. в любом случае.
5. В этом случае в принципе транзакции нам нужны только для «все или ничего с ролбэками». Боюсь что без этого транзакции убьют перформанс на нет когда все и вся будет ритраиться из-за конфликтов.

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

В реале речь идет о бронировании, не лотах. Так же бизнес b2b и каждая проблема с неправильными данными грозит потерей клиента. Например если вдруг 2 человека забронировали вещь, придется одному из них отказать, и возместить. Но с монги уйти не можем.

Не уверен что всех этих проблем можно избежать полагаясь только на транзакции. Тем более придется разбираться очень хорошо с изоляциями транзакций (например Serializable как я понимаю монго не поддерживает). Всякие фантомные ридс и т.д. Я то в конце разберусь, а вот остальные люди в компании — не уверен что все разберутся.
НЛО прилетело и опубликовало эту надпись здесь
В чате сообщества @dddevotion недавно обсуждали аренду велосипедов. Можно поискать к чему коллективный разум пришел.
Спасибо! Загляну! а нет случайно таких же каналов для слака?
Сенкс! Будем посмотреть.
Непонятно насколько большая конкурентность за один айтем.

Есть подходы для высококонкурентных запросов, например билеты на матч в старт продаж.
Но если у вас не столь конкурентный паттерн, то я бы делал в том же ключе, что и oxidmod
А где можно почитать про эти подходы?
Попадалось несколько раз, но толком не помню. Можно посмотреть паттерн Space Based в Fundamentals of Software Architecture
Зарегистрируйтесь на Хабре, чтобы оставить комментарий