Обновить
13
Руслан Тагиев@RuslanTaghiyev

Engineer/Architect

7
Подписчики
Отправить сообщение

Подходы взаимодействия через интерфейсы и ТД(pub/sub, partnership, separated ways, shared, etc) - это какраз все про взаимодействие контекстов, а не доменов.

Тут как раз таки скорее у вас ошибка. Во первых важно не путать концепты интеграции БК такие как partnership, separated ways, conformist, с реализациями типа pub/sub или синхронный вызов/sync API – но я в целом не про них говорил. Я говорю про взаимодействие сабдоменов ВНУТРИ БК(Bounded Context). Подходы через интерфейсы и тд никак напрямую не зависят от интеграций БК. Если вы условно создаёте модульный монолит который состоит из одного БК, то ваши модули всё равно будут взаимодействовать через те-же интерфейсы. По хорошему все авторы сходятся во мнение что 1 БК в идеале должен быть 1 сабдоменом. Если же говорить чуть более приземлённо, то в рамках одного БК может быть несколько саб-доменов и их взаимодействие никак не относится к интеграции БК.

Вот пример чуть нагляднее – представьте что у вас 1 БК, но 2 микросервиса(никто не запрещает), один реализует supporting сабдомен, другой core. Как они будут у вас общаться? Так или иначе через API(и не важно синхронный или ассинхронный). Так почему тогда в рамках монолита всё должно лежать в 1 пакете и иметь иерархичную структуру? И как вы тогда если что сможете их в случае нужды декомпозировать на микросервисы?

А solution architect обычно вообще не прибегает к коду и делегирует эти задачи команде разработки, он больше манипулирует другими вещами - такими как ендпоинты/схема бд и тд

Ну это уже от архитектора зависит. Я вот, как software architect очень даже манипулирую :D

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

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

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

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

Представьте, что product состоит из нескольких агрегатов и множества ентити, мы получим в папке entity много файлов и много подпапок

Если говорить только про кол-во файлов – не совсем понимаю, а как это отличается от вашего подхода? В нём по идее ещё больше файлов будет в корневом пакете, тк энтити, VO и тд на одном уровне. А про подпапки я вообще ничего не говорил :D

В данной картине очень сложно понять кто рут домен а кто сапортящий

Почему же? Я считаю что сабдомены должны быть на одном уровне и общаться друг с другом по хорошему только через интерфейсы-API. На сколько я помню ни Эванс ни Вон Вернон не говорили про вложенные сабдомены. Тот же Влад Хононов в книге с обезьяной предлагает дробить сабдомены на более гранулярные например, и иметь плоскую структуру вместо вложенной если это неоходимо. Даже если мы решаем что нам могут быть нужны вложенные сабдомены, то имхо это должно обрабатываться по другому. Если это монолит - то делать его модульным и как минимум верхнеуровневые сабдомены делать отдельными модулями. Если же это микросервисы то тут вообще не нужно много думать - либо у вас 1 (верхнеуровневый) домен/сабдомен на микросервис, либо опять же делаете его модульным.

Т.е. имхо не должно быть такого:

/core_subdomain
    /supporting_nested_subdomain
    some classes of core subdomain

Если уж нужны вложенные сабдомены, то делать что-то вроде такого:

/core_subdomain 
    /supporting_nested_subdomain
    /supporting_nested_subdomain_client
    /core_nested subdomain -> depends on supporting_nested_subdomain_client, not supporting_nested_subdomain

Ну а вообще стоит проанализировать точно ли это вообще сабдомены или может просто модули.

Спасибо за статью, интересно увидеть альтернативный взгляд.

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

Основной минус - сложность для понимания

Вы утверждаете что с такой структурой проще понимать доменную логику:

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

Я бы сказал что наоборот. Когда вы анализируете код проекта(и не важно потому что вы разработчик который только пришёл на него, или например архитектор который собирается его декомпозировать), то вам не настолько принципиально какие там доменные ивенты или ВО. Самое главное это понять какие есть агрегейт руты и энтити и дальше уже копать вглубь, чтобы понять как модуль/сабдомен взаимодействует с остальными или какие фичи он реализует и тд. И я не согласен с утверждением что отталкиваться нужно от фич:

Нам не интересно ProductPrice это VO или Entity, нам важно какую бизнес задачу решает этот объект. Мы видим бизнес-фичу "Назначить продукт на шаблон" и все её внутренние состовляющие которые она в себе содержит

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

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

Минус второй - сложность анализа

Помимо этого, намного проще визуально анализировать сложность доменного слоя когда он разделён на чёткие слои - например если у нас 100 сущностей и 5 VO, скорее всего мы делаем что-то не так. Суда же относятся и кол-во доменных сервисов. Вы в статье утвержаете что доменный сервис и фича это одно и тоже:

Фичи (aka доменный сервис) именуем в повелительном падеже: Сделай что-то. Создай товар. Обнови цену. Проверь имя на уникальность. Посчитай очки популярности.

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

Вот например из книги Эванса:

“When a significant process or transformation in the domain is not a natural responsibility of an ENTITY or VALUE OBJECT, add an operation to the model as a standalone interface declared as a SERVICE. Define the interface in terms of the language of the model and make sure the operation name is part of the UBIQUITOUS LANGUAGE. Make the SERVICE stateless.”

...

"SERVICES should be used judiciously and not allowed to strip the ENTITIES and VALUE OBJECTS of all their behavior."

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

Можно конечно решать эти проблемы соответствующими аннотациями(т.е. помечать например @ValueObject или @AggregateRoot) или интерфейсами + fitness functions, но имхо это сложнее реализовать, менее наглядно, не всем нужно и не позволяет проводить визуальный анализ.

Алтернативный пример

Вот пример структуры которую использую я в монолитах:

/domain
    /product
        /entity
        /service
        /value_object
        /event
    /another module/subdomain

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

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

Ну и напоследок

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

  • Заставить разработчиков вникать в бизнес. Если они это делают то они и с анемичной моделью будут фокусироваться на бизнесе и использовать стратегические паттерны DDD. А если нет, то никакие тактические паттерны DDD вам не помогут, скорее наоборот.

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

  • Иметь единую структуру пакетов и тактических паттернов в рамках проекта и организации. Если вы планируете проекты которые будут расти и поддерживаться годами то стандартизация имеет огромное значение. Куда важнее чтобы структура пакетов была одинаковой в разных модулях/сервисах, чем то, чтобы она была "правильнее", но только в некоторых. И чтобы у всех было понимание какие тактические паттерны используются на проекте – чтобы не было ситуаций когда кто-то использует анемичную модель, кто-то DDD, кто-то смесь и тд.

Спасибо за перевод!


Кстати последняя ссылка:

кто придерживается иного мнения…

уже невалидна, но страницу можно найти в web-архиве.

Так а где в статье вообще говорится о том, что задача архитектора — это только записывать NFR-ы, или что архитектор может не иметь опыта?

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

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

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

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

Поверьте, если бы все постоянно про это не забывали, я бы этого не добавлял.

Но не менее важным является BDUF

Возможно, у вас какое-то своё понимание BDUF, но традиционно он ассоциируется с Waterfall'ом и тем, что мы дизайним всё и сразу — в противовес эволюционирующей архитектуре.

Есть еще принцип DRY

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

Нет, нет, и еще раз нет!

Так а что конкретно нет?

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

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

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

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

Другой пример хорошей архитектуры - это человек.

Вот только человек это буквально пример эволюционирующей архитектуры.

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

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

Богатая модель помещает логику внутрь сущностей (order.Complete()). Это идеально ложится в ядро "лука".

Ну вот именно поэтому я не вижу, как она может быть реализована в ФП без костылей и нарушений правил ФП. Поэтому придерживаюсь термина "Доменная модель", если говорю просто о какой-то абстрактной модели. А дальше она может быть Богатой, Анемичной, Функциональной и т.д. Хотя, конечно, у тех же ООП-приверженцев зачастую просто "Доменная Модель" = "Богатая Доменная Модель", с чем я уже не согласен.

С другой стороны, богатую модель принято связывать с ООП (защита инвариантов объектов). Может быть, поэтому автор и не употреблял терминов богатая или анемичная модель.

Вообще, очень жаль, что все термины буквально засраны следами ООП.

Ну так это неспроста. Эванс и Фаулер были, даже если не изобретателями большинства терминов, то как минимум их популяризаторами. Да и если смотреть на процент распространения ООП и ФП-языков и архитектур/подходов, то первых несравненно больше. Будь ФП сейчас таким же популярным, то как вы говорите всё было бы "засрано" следами ФП.

Не читайте. Устарело на 20+ лет

А как по вашему ООП или DDD изменились за 20+ лет? :)

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

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

DDD (стратегические паттерны) так точно не обязательно применимо только к ООП. А вот насчёт богатой модели — не уверен. Если говорить про ФП — там одна из главных идей в чистых функциях. Сложно представить, как богатую модель совместить с чистыми функциями. У меня давно в списке для чтения лежит книжка про функциональное DDD — я её пару раз пролистывал, но ничего про богатую модель не видел. Поэтому буду рад если вы сможете привести примеры.

Можно подробней? Из ссылки не ясно

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

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

Приведу пример для уровня изолированности READ_COMMITTED (дефолтного для подавляющего большинства проектов с реляционными БД):

Предположим, у вас есть два аккаунта:

  • Account 1: balance 1000

  • Account 2: balance 1000

Два процесса параллельно пытаются изменить эти данные. Первый процесс читает баланс аккаунта 1, который равен 1000, снимает с него 100 и добавляет 100 на баланс аккаунта 2. Второй процесс также читает баланс аккаунта 1, который равен 1000, снимает с него 200 и добавляет 200 на баланс аккаунта 2 (будем считать что мы не используем атомарный инкремент/декремент).

Правильным результатом было бы:

  • Account 1: balance 700

  • Account 2: balance 1300

Но в зависимости от того, какой из процессов завершится первым, мы получим либо:

  • Account 1: balance 900

  • Account 2: balance 1100

Либо:

  • Account 1: balance 800

  • Account 2: balance 1200

Т.е. результаты одной из транзакций просто проигнорируются.

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

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

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

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

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

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

А зачем и что это изменит?

Инжектя интерфейс мы завязываемся на абстракции. Таким образом, мы можем в любой момент заменить реализацию без изменения вызывающего класса. Также это один из принципов SOLID(который D).

Может быть полезно для подстановки мок реализаций в тестах например.

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

Спасибо за комментарий, давно увидел его, никак руки не доходили исправить)
Не спорю, данный вариант тоже подходит, но суть архитектуры в том, что она завязана на дженериках и трансферах, а в случае, который советует Блох получается сильная привязка к enum — у, чего мне совсем не хотелось, т к данное поле изначально не являлось обязательным для бизнес логики.
1. Добавил фильтр по дате.
2. Компилятор ни на что не ругается, если убрать try/catch
Вы были правы на счёт даты. Вывод в логи тоже добавил.
Выбирать всех пользователей и потом фильтровать их это дурной тон. Представьте, что у вас миллионы пользователей. Вы их начнете втаскивать в память из базы.

А что вы предлагаете?
Разберите Exception e на несколько тех что реально бросаются, чтобы не подавлять RuntimeException

Как именно я могу узнать все возможные исключения?
1

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность

Специализация

Бэкенд разработчик, Архитектор программного обеспечения