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

Чем меня не устраивает гексагональная архитектура. Моя имплементация DDD – многоуровневая блочная архитектура

Время на прочтение 7 мин
Количество просмотров 8.6K
Всего голосов 7: ↑4 и ↓3 +1
Комментарии 22

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

@kolkoni, мне кажется что вы нарисовали гексагональную архитектуру "в профиль". Ведь Гексагональная архитектура - это ни что иное, как "Архитектура портов и адаптеров". Алистар Кокбёрн ввёл туда многогарнник лишь для того, чтобы упростить визуализацию. А конкретнее:

The hexagon is not a hexagon because the number six is important, but rather to allow the people doing the drawing to have room to insert ports and adapters as they need, not being constrained by a one-dimensional layered drawing.

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

>Алистар Кокбёрн

*Коубёрн

Смысл гексагональной архитектуры только в том, чтобы направления зависимостей были только «внутрь кольца». А вот каноничная многоуровневая архитектура — это когда все зависимости идут строго в одном направлении, и это совсем не так, как у вас. Важно понимать, что в обоих архитектурах по сути действия идут снаружи до базы и обратно, но именно в гексагональной архитектуре направление зависимостей и направление потока данных не совпадает — это и есть dependency inversion. И именно это свойство отличает гексагональную от уровневой.

Я и не говорю, что делаю каноничную… Я сделал ту, которая мне сильно упростила работу, при этом соответствует моим критериям "красоты". Делал на основе гексагональной, которую изначально пытался взять за основу, но наткнулся на неудобства.

Я не понял, почему вы считаете, что ваша логика «не замкнута в гексагон». Вы тут по сути только добавили ещё один круг бизнес процессов (что в книге Вон Вернона тоже есть) и больше никаких отличий от каноничного способа реализации DDD я не вижу. Действительно важная проблема — вы тут не выделяете агрегаты и позволяете себе атомарно выполнять действия над разными сущностями не только одного, но и разных доменов. Будь у вас более сложная модель данных в базе — это бы вышло боком по из-за неминуемых проблем с производительностью. Я бы всё-таки порекомендовал вам почитать книгу Вон Вернона. Она хоть и большая, но в ней даны многие практические советы по реализации DDD, чтобы вы своим велосипедом по граблям не катались.
  • А где Вы тут видите замкнутость? Уже в гексагональной замкнутости нет, потому что добавились стороны, а это уже значит что форма и визуализация неправильно подобраны.
  • Нету каноничной реализации, есть разные имплементации и то совсем не полные.
  • Приведите пример хоть сколько нибудь работающего приложения, где операции над сущностями разных доменов не выполняются одновременно? Невозможно реализовать работающий бизнес процесс только внутри одного домена, у Вас в итоге получится один огромный домен.
  • Я биллинговую систему частично перевел на эту архитектуру, никаких проблем. А модели там раз в 20 сложнее, чем в примере.
  • Приведите хоть один практический совет по реализации из книги, который не вписывается в эту архитектуру. Я не пытаюсь DDD переиначить, просто вывожу практический опыт в теоретическое поле.
Приведите пример хоть сколько нибудь работающего приложения, где операции над сущностями разных доменов не выполняются одновременно? Невозможно реализовать работающий бизнес процесс только внутри одного домена, у Вас в итоге получится один огромный домен.


Так в этом и сложность DDD, что надо с одной стороны обеспечить изоляцию поддоменов, с другой — не нарушить инварианты всех агрегатов, а с третей — не просадить в ноль производительность. Вы же срезали кучу углов видимо в погоне за тем, чтобы назвать это DDD, но он и близко так не делается. Ваша статья — это просто какой-то пример реализации. Методологически она скорее вредна тем, кто подумает «так вот как DDD делать надо».

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


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

В этом примере также типичные антипаттерны типа анемичной модели, из-за чего логика работы объекта полностью переезжает в ваш слой «бизнес процессов» и появляется low cohesion со всеми вытекающими, вследствие чего смысла в отдельных доменных объектах практически не остаётся.

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

Примеры?!


Методологически она скорее вредна тем, кто подумает «так вот как DDD делать надо».

Чем именно? Конкретный пример, что тут не так. Или Вы пустозвон уважаемый?


Большие графы объектов похоронят вашу производительность при таком подходе.

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


В этом примере также типичные антипаттерны типа анемичной модели, из-за чего логика работы объекта полностью переезжает в ваш слой «бизнес процессов» и появляется low cohesion со всеми вытекающими, вследствие чего смысла в отдельных доменных объектах практически не остаётся.

Теперь Я точно уверен что Вы не понимаете о чем пишете. Просто сотрясаете воздух терминами, в которых не разбираетесь.
Тем, кто будет читать данный комментарий: просто прочитайте про анемичную модель и сравните с тем, что пишет данный индивид выше.


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

А, Вы предлагаете поступить как Вы — "не понимаю, ну и ладно, буду кидаться сложными терминами в комментариях". Нет, спасибо.


Сделайте CRUD и если оно заработает для вашей задачи — ну и замечательно.

Ну оно и видно, что Вы сложнее CRUD-а в своей жизни никогда ничего не писали.


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

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

С этой стороны не смотрел, есть такое. Тут скорее описание как сделать средний слой)

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

Согласен с критикой.
По поводу инъекций, тут можно нагородить как в Nest-е, но для меня это кажется избыточным, а так да, можно DI полноценный реализовать и не руками внутрь пробрасывать, а через DI.
Да, каждый вышестоящий слой связан с предыдущим, а адаптеры связаны как с предыдущим так и с вышестоящим (на картинке в виде выступов на самом блоке). Но если не брать теоретические модели, то в практике полностью несвязанные сущности сделать либо невозможно, либо овчинка выделки не будет стоить, по сложности. Поэтому есть абстракции в виде адаптеров, чтобы не быть связанным с конкретной БД или конкретным API.

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

Встречная идея такая: как насчёт отказаться от иллюзорной независимости доменов и разрешить себе композицию доменов и/или доменных методов? Надо же чему-то хорошему у функциональщиков учиться.
• У вас в реальности есть юз-кейс создания клиента — реализуем его в коде доменным методом createClient().
• У вас в реальности есть юз-кейс добавления существующего клиента в партнёры — доменный метод addClientToPartners().
• И у вас есть юз-кейс создания клиента-партнёра, который является композицией предыдущих двух, — оформляем его в виде доменного метода createPartner(), который использует createClient() и addClientToPartners().
Да, получается, доменный метод createPartner() в этом случае зависит от двух других доменных методов. Ну и ничего страшного, потому что и реальный юз-кейс зависит от двух других. Наличие таких зависимостей не делает доменный метод новой сущностью (бизнес-процессом). Это всё ещё просто доменный метод, к которому мы можем прибивать порты. И, да, порты могут быть прибиты в этом примере к любому из трёх доменных методов (хоть к атомарным, хоть к композитноум), потому что все три юз-кейса существуют в реальности и могут работать по отдельности. И, да, нужда в группировке доменных методов в домены вроде как отваливается, потому что каждый публичный доменный метод, реализующий свой юз-кейс (хоть атомарный, хоть композитный) — это и есть как бы самостоятельный маленький домен.

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

То есть, внизу адаптеры, а над ними — граф юз-кейсов, к которым сверху прибиты порты.

Понятно объяснил? Покритикуйте.

Объяснили понятно, спасибо.
По моему DDD не совсем про это. Домены обособляются не потому что "так надо", а потому что каждый домен — это большой пласт нюансов, который по хорошему должен разрабатываться отдельной командой, но при этом чтобы был понятен другим и его можно было легко протестировать и использовать.
А что мешает задублировать внутрянку бизнес процесса? Это даже не будет нарушением DRY, потому что бизнес процесс может измениться (сейчас он похож на другой, а потом станет не похож) или отличаться маленьким нюансом, из за которого придется дочерний бизнес процесс переписывать и учитывать этот нюанс, а это уже приводит к неоднозначности ответа от бизнес процесса, что нарушает те маленькие правила, которые Я для себя вывел.
К тому же если так сгущать краски, можно любую архитектуру "запороть", важно соблюдать баланс. Я например не могу придумать сценарий, где "придется" городить супер бизнес процесс.
По поводу предполагаемого метода createPartner, это сработает, пока нет каких то особенностей. Но как только какая то часть внутри усложнится метод createPartner превратится в 100-строчный код с кучей if-ов, уже видел неоднократно.
Речь идёт не про то чтобы код был красивым после написания приложения, а про то чтобы его можно было поддерживать и расширять, потому что бизнес меняется, условия меняются и надо быстро и удобно развивать приложение. Это кстати огромная беда, что разработчики думают только про момент запуска приложения и совсем не думают о последующих этапах жизненного цикла.
Ещё одной проблемой является тестирование, подмена всех зависимостей в композитных методах — это будет адище.
По поводу сложности приложения — про DDD начинают задумываться после прохождения определенной точки сложности проекта, когда добавление маленькой фичи оборачивается огромным пластом работ. Вы предлагаете метод от сложного к простому, но обычно проект развивается от простого к сложному и появляется необходимость во всём вот этом. Когда есть маленький сервис, конечно DDD, даже в минимальной своей реализации избыточен.

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


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

А что мешает задублировать внутрянку бизнес процесса? Это даже не будет нарушением DRY


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

Я например не могу придумать сценарий, где «придется» городить супер бизнес процесс.


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

Но как только какая то часть внутри усложнится метод createPartner превратится в 100-строчный код с кучей if-ов, уже видел неоднократно.


вот именно появление if-ов и будет очень наглядным сигналом к тому, что бизнес-процессы стали различаться и пора рефакторить: у вас теперь createPartner() не использует стандартный addClientToPartners(), а добавляет в список партнёров как-то иначе. и это нормальная реакция кода на изменение бизнес-процесса в реальности.

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


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

Вы предлагаете метод от сложного к простому


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

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

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


Ну и логика (отката)транзакций — совершенно не бизнес-логика.

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

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


Если подумать — гениальная бизнес-модель. Технологии, особенно интеграция, стоят довольно дорого, несамостоятельные программисты (могут делать) и книжные архитекторы (могут по книжке) и менеджеры-атжальщики (могут управлять) стоят по-отдельности вполне умеренно. Добавляем методологию, которая гарантирует "средний" результат, и вуаля — профит!


Какую бы вы ни придумали методологию, большинство ее пользователей — ленивые формалисты, использующие в работе в основном "обезьяний" мозг (в терминологии книги "Думай медленно, решай быстро") и обитающие как раз в местах, которые построены вокруг методологий — иерархические структуры, где удобно держать штат "недоразвитых" инженеров и решать все проблемы на уровне управления (почти как RISC микропроцессор и компилятор). Ну и компании уже на пути к банкротству, где не поняли как правильно монетизировать книжные методологии в IT.


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

Насчет луковой, в более понятном виде, если ее разложить по вертикали:
1. Фундамент внизу — это платформа (Java/.NET/и т.п.)
2. [опционально] На фундаменте может стоять какой-то свой фреймворк-обертка, помогающая писать меньше кода
3. Далее стоит Domain (который нарисован в центре диаграммы луковой архитектуры). Эта сборка зависит только от фундамента. Так как базовые типы (int, string) определены именно в фундаменте. Содержит какие-то классы-реализации + интерфейсы, которые будут реализованы на слоях выше (дальше от центра)
4. Далее идут Domain/App Services, содержит классы-реализации для интерфейсов из Domain + другие интерфейсы, которые будут реализованы на слоях выше (дальше от центра), например IDb, ICache, IExternalApi и так далее.
5. Далее по аналогии для Controllers/Tests/UI
6. Еще выше (дальше от центра) — это всякие обертки над инфраструктурой (базами данных, внешними системами и так далее). Тут у нас например конкретная ORM.
7. И на самом верху уже конкретные инфраструктурные вещи (конкретная БД, MS SQL или Oracle)

Каждый слой для работы использует интерфейс, реализация которого лежит на более высоком уровне и на самом деле неважно где именно. Controller на уровне 5 будет использовать IDb, который определен на уровне 4, но реализация IDb будет на уровне 6. Все это относительно легко собирается IoC контейнером.

Проблемы тут следующие (касается платформы .NET):
1. Использование ORM (она у нас на 6 уровне) чем-нибудь да протекает вплоть до Domain (уровень 3). У EF например протекает DbSet (он порой нужен в IDb для обобщенных задач), маппинг классов domain на таблицы (хотя их можно переместить в сам контекст, просто будет god-метод) и предполагаю некоторые специфичные атрибуты (хотя пока что, все, которые я использовал, лежали в System.ComponentModel.DataAnnotations, т.е. в фундаменте). На 100% защититься интерфейсами мне пока что не удалось.
2. Некоторые проблемы с Identity. Сущности наследники от AppUser, AppRole находятся в Domain. Но они решаемы — можно выделить в отдельный домен или на уровне 6 сделать еще один слой сущностей для ORM и маппингом между ними и сущностями из Domain.
3. Меня так же смущают модели для UI и API и их маппинг с классами из Domain. Там в любом случае требуется зависимость сборки Models от Domain. Эта сборка у меня получилась между уровнем 3 и 4. Опять же проблема решаемая — перекладываем маппинги на более высокий уровень (4-ый или 5-ый), но тем самым мы порой будем тащить весь 4-ый уровень везде, даже туда, где требуются только модели.
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории