Pull to refresh

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>.

Sign up to leave a comment.

Articles