Comments 15
Так а зачем люди нужны port? Если есть adapter in/out?
Насколько я понимаю Дядю Боба - это интерфейс (Java) или абстрактный класс (C++), который определеяется и декларируется сервисами (что им нужно). А адаптеры реализуют эти сервисы и направляют запросы к соответствующим библиотекам и БД. Необходимо для того, чтобы сервисы понятия не имели о деталях реализации БД и прочих библиотек Написав адаптер, можно воспользоваться Хибернатом, а можно другой библиотекой, или переделать сохранение платежных документов не в БД, а в NoSQL.
Интерфейсы в пакете application.port
обеспечивают ту самую слабую связность из статьи.
Это ширма между бизнесовым кодом и адаптерами, которые взаимодействуют с внешними системами.
Ещё, можно о port думать как о контракте внутренней логики:
То, что лежит в application.port.in
описывает как внутреннюю логику можно вызывать.
А то, что лежит в application.port.out
ожидает от реализации соответствующих адаптеров в adapter.out
С одной стороны позволяет подменить один адаптер другим.
Например, наше приложение использует кэш:class MemcachedAdapter implements CacheGetPort, CachePutPort
В любой момент мы можем подменить этот класс другим главное чтобы он реализовывал интерфейсы CacheGetPort
, CachePutPort
С другой стороны, для адаптеров позволяет отвязаться от конкретной реализации из application.domain.service
. Не завязываться на внутренний класс. Ну и тестировать внутреннюю логику исходя из того что это "Чёрный ящик".
Статья классная, только мне не нравится упор на "гексагональная". Гексагональность или пятиугольность - не имеет значения и смысла. Это раз.
Второе - ни словом, ни полсловом не упоминается Дядя Боб (Роберт Мартин), концепцию чистой архитектуры которую, вы применяете. Неприлично.
Читая статью, первый тревожный звоночек зазвенел в самом начале, когда без объяснения почему: что решается, включая бизнес и слова заканчивающиеся на ...сть - сразу перешли к как - к решению.
Второй - когда прочитал про полную загрузку данных. А если бизнес требования изменятся так, что надо будет процессить например 500 Гб данных из например базы? Поставим машину с теребайтом оперативки? В общем унификация подхода к критически разным поставщикам данных: БД, Кафка, веб - выглядит рискованной.
Колокол громко забил в набат, прочтя о коммитах в 100+ файлов. Есть такая метрика: коннасценция(сonnascence), определяющая эффективную связанность. Коммиты в 100+ файлов говорят, что коннасценция крайне высока. Так вот она пересекается с coupling. Хотя вроде ТС пишет о попытке его снизить...
Потому понять что получилось нельзя - равно вероятен любой вариант от ужасно до замечательно.
Сколько не пытался понять смысл подобного в Spring бэк-проектах, так и не смог. Выглядит усложнением, которое не приносит больших плюсов. Спринговый код и так во многих случаях основан на POJO и аннотациях, не сказал бы, что там есть сильная связанность по чему-то кроме интерфейсов. Захочешь подменить реализацию - это легко сделать. Чуть сложнее избавиться от ORM, но про ORM и тут не все понятно.
Плюс ко всему еще надо и новичков погружать в это, ломать устоявшиеся привычки.
Отчасти согласен с Вами. Спринг создавался как попытка реализовать "Чистую архитектуру". Грамотное и осознанное применение спринга, с пониманием, зачем спринг нужен и какие он проблемы решает, с преследованием реализации чистой архитектуры, делает вышеприведенные "наслоения" и "архитектуру" излишней.
И да, согласен, Хибернат противоречит идее чистой архитектуры.
Как и у любого инструмента, у этого подхода есть как плюсы, так и минусы. А главное - место применения. И вот тут кроется проблема. Для большинства приложений она избыточна, на мой взгляд. А понять нужно ли тебе будет на ходу менять реализацию, изначально скорее всего ты не сможешь.
То что решает данная архитектура можно спокойно сделать одним способом (не называя это архитектурой) - разделяй уровень представления и бизнес логику. И для этого в java уже давно всё есть - dto, mapper, и паттернов адаптер, фасад.
Но простого способа понять утекли ли зависимости я не нашёл
Решается модулями. Пакеты не могут полностью обеспечить гарантию скрытия подробностей, если у вас более одного уровня вложенности.
Мы начинали с пакетов, потом заметили утечки зависимостей и порезали слои на модули, теперь всё отлично.
Мы уже более пяти лет достаточно успешно используем гексагональную архитектуру в своих продуктах. У нас немного другая терминология (к примеру вместо Incoming и Outgoing Adapters мы используе термины Driving и Driven Adapters) и более сложное разбиение слоёв на пакеты, модули и проекты, но суть такая-же. За это время мы обросли набором абстракций для каждого слоя, и вынесли туда весь "скучный", рутинный код и его тесты, что позволяет быстро добавлять новые предметные области, уменьшает вероятность ошибок и сокращает время ревью. Наш опыт показал, что архитектура достаточно быстро понимается новыми членами команды и сокращает время их адаптации.
Наш софт сертифицирован в ЕС как "медицинский продукт" и в нашем случае наличие большого количества UseCase портов огромное благо. Это позволяет доказуемо покрывать все эти use cases тестами, так как одним из аспектов сертификации является requirements traceability, и за наличием таких тестов следят на сертифиционных аудитах.
Хотел ты посоветовать вам посмотреть в сторону ArchUnit тестов, они помогают отслеживать нарушения границ слоёв, отсутствие взаимодействий между адаптерами минуя core и многое другое.
На мой взгляд, возможно, эта архитектура подойдет для монолита, но ни как не микросервиса. Для микросервиса лучше пользоваться принципом одной ответственности, тогда и не надо будет делить на домены и складывать 999 бизнес сервисов в один пакет. В плане UseCase’ов, если в конструкторе сервиса или репозитория не больше 8 инекций, то смысла вообще нет. Для новых членов команды вообще беда. У нас были такие сервисы, перебрали, вздохнули с облегчением ?
Или по доменам: весь код, относящийся к конкретному объекту отправляем в один пакет
Это называется либо вертикальный слайсинг, либо разделение bounded контекстов в зависимости от используемого подхода. "По доменам" это называть не совсем правильно. Домен - он один. В рамках баундед контекста выделяется часть домена выделяется в необходимую для сервиса доменную модель
Как следствие, внедрение новых сотрудников занимает гораздо больше времени, чем могло бы.
Так могло бы быть, в случае идеальной картины мира, когда разработчики представляют, что такое гексагональные типы агрикультур и как их готовить. На практике, у нас это очень редкое явление, в то время как про трехслойку знают все. И обычно онбординг из-за этого занимает все же больше времени. Другой вопрос, что когда ты уже разобрался с той интерпретаций соответствующего подхода в команде, то разобраться в следующем сервисе действительно становится проще.
В директории гексагонального проекта вы увидите такую структуру...
1) Нет, не увидите. Это уже гораздо более поздние интерпретации. Как вариант луковичная или чистая. Гексагональная архитектура говорила о физическом разделении адаптеров. Каждый адаптер поставлялся в отдельном джарнике, в то время, пока аппликейшн завязана на порты. И это имело строго техническое применение. На дворе, сюрприз, 2005 год. Спринг только год назад в релиз вышел, про ваши девопсы никто даже не слышал. Скорости инета не такие большие. Люди работали на ee стеке. Собирали нужные джарки, по какому нибудь фтп/ссх закидывали их на серв где крутится большой и страшный аппликейшн сервер и благодаря такому подходу могли хот релоуднуть нужный адаптер заменив на новый, в то время как остальное приложение фактически продолжало работать... Идея, что тот же подход можно использовать и в рамках одного проекта появилась на несколько лет позже. Всякие спринги с идеей тупо поднять отдельный сервлет контейнер и перенаправить на него трафик вместо этих сложностей с ее серверами к тому времени активно набирало популярность. И собственно луковичная архитектура как раз об этом. Да, мы не делаем хот релоуды, да, у нас сейчас одна запакованная джарка и все хорошо... Но идея, что во главе стоит бизнес логики, а не контроллеры с репозиториями - здравая и давайте продолжать ее использовать...
2) 1 common и на application и на адаптер... Даже не знаю, что кроме портов с их дто-шками туда можно положить. Но тогда почему бы их так не назвать? Exception-ы ещё, но они обычно тоже отдельно как-то отдельно выносят. Но судя по схеме порты лежат в аппликейшне... Но тогда я совсем не понимаю, что там находится. Они разные и шарить между собой ничего не должны.
| ├── domain/
│ │ ├── model/
│ │ └── service/
Да откуда вы блин все это берёте? На тех схемах, на которые вы же и ссылаетесь домен лежит внутри аппликейшн слоя, показывая, что есть доменная модель, и на базе ее строится аппликейшн. Таким помещением в рамках внутреннего слоя подразумевалось, что домен, как самая внутренняя часть проекта, его сердце, ни на кого не зависит а обозначает сущности, существующие (простите за тавтологию) вне зависимости от существования твоего приложения. Т.е. если бы его не было, все равно люди бы бегали вокруг и на условных бумажках это все делали... А аппликейшн слой - то уровень логики твоего приложения, которая, естественным образом зависит от домена... Что блин это за попытки переложить бл в домен? Причём повальная...
Но простого способа понять утекли ли зависимости я не нашёл
Плохо искал. Закон протекающих абстракций не вчера был придуман и пока это были физически разные проекты, которые просто не имели зависимостей друг на друга и ты не мог обратиться к спринговому классу в бизнес логике просто потому, что спринга там не было, то с луковой архитектурой эта проблема уже появилась... И ее учились решать. Сначала каким-то самописными системами проверки, потом появился ArchUnit... и кстати, идея о том, что бл не должна зависеть от фреймворков сформирована только уже только в чистой архитектуре, первые заметки о которой были в 12 году... Быстро вы 7 лет эволюции конечно :D
На это нужно обращать внимание при ревью нового кода.
Такая себе идея. Во время ревью кода нужна верхнеуровневая проверка. Автоматика не знает особенностей ваших систем и ревьюеры проверяют, можно ли делать то, что ты делаешь. А проверять стили/зависимости, наличие всяких очевидных плохих практик и пр. должна автоматика.
Гексагональная модель пушит нас для ядра приложения использовать свою внутреннюю модель.
Это не гексагональная модель пушит... Люди и наигравшись со слоенкой приходят к тому, что это не адекватно, что когда у тебя меняется апи - падает работа с бд, потому что какое-то поле по другому завется... Но почему? Я ведь апи меняю... А если тебе придётся на бд другую съезжать, а если вдруг у этих бд окажется нужно по разному схемы строить потому что какие-то свои особенности... Не, я понимаю, что конвертеры уже наплохокодили, но... Если уже наступили в лужу и какие-то костыли заиспользовали - окей, но может лучше не наступать?...
Мне нравится такой подход - позволяет безболезненно декомпозировать "микролит" в случае необходимости. Только разве EventService - класс с бизнес логикой должен зависеть от фреймворка? По мне как лучше использовать java-конфигурацию бинов.
Я не понял про утечку зависимостей. Зачем вы в интерфейс адаптеров FindClientsPort
прописали Pageable от спринга? Там должны быть типы, требуемые бизнес-слою Application.Domain.Service. Как адаптер обратится к базе - с помощью jpa, odbc, или еще как - головная боль адаптера. Интерфейс должен определять, что findAll
должен принимает что-то типа (integer first, integer count) и возвращать List<Client>.
Гексагональная Архитектура и Spring Boot