Комментарии 9
Гексагональная архитектура это конечно хорошо, но вы забыли упомянуть об количестве кода, который порождается таким подходом, особенно количество pojo/dto/model классов, т.е. просто пустых классов с полями:
-сама модель
-value object для модификации объекта модели
-дто для rest-интерфейсов
-jpa-entity для бд
Для каждого перехода с одного типа на другой потребуются свои мапперы, сервисы и т.д.
И все это на 90% случаев ради того, чтобы сохранить json в бд. Гексагональную архитектуру следует применять только для того подмножества доменной области, которая сдержит в себе непрерывные процессы моделирования, то есть некоторые объекты, которые живут в памяти со старта приложения, крутясь в цикле, например - серверная часть мультиплеерных игр.
Если же у вас request/response схема работы с данными, то лучше даже не влезать в гексагональную архитектуру, или влезать, но моделью данных сделать непосредственно jpa-entity
Это уже скорее вопрос перфоманса. Советую посмотреть доклад от тех, кто реально этим занимается: Роман Елизаров — Миллионы котировок в секунду на чистой Java
Работать с jpa-entity вроде как плохая практика, тк энтити будет перемещаться между транзакт методами (скорее всего помечены @Trandsctioal) , она становится detached и возможны сайд эффекты всякие неприятные, нет ?
На Java автор потратил 6 модулей, 500+ строк pom.xml и танцы с maven-enforcer-plugin, @ComponentScan, фабриками для value objects...
На Rust это же:
// Порт — просто trait
pub trait CreditRepository: Send + Sync {
fn save(&self, credit: &Credit) -> Result<(), Error>;
}
// Value Object — newtype, без фабрик
pub struct CreditAmount(Decimal);
impl CreditAmount {
pub fn new(v: Decimal) -> Result<Self, Error> {
(v >= Decimal::ZERO).then(|| Self(v)).ok_or(Error::Negative)
}
}
// Сервис — generics вместо DI-контейнера
pub struct CreditService<R: CreditRepository, T: TimeProvider> {
repo: R,
time: T,
}Автор на Java: 6 модулей, куча XML, плагин чтобы запретить зависимости, фабрика для даты, Spring чтобы всё склеить.
На Rust: trait это и есть порт. Структура с приватным полем это и есть value object. Передал зависимости в конструктор, вот тебе и DI.
Нет null - не нужен jspecify. Модуль не видит чужие зависимости - не нужен enforcer-плагин. Хочешь мок в тесте - просто передай другую структуру.
На Java ты настраиваешь фреймворк чтобы он не мешал писать чистый код. На Rust ты просто пишешь чистый код "из коробки".
Кратко, на ощущениях:
Go: Самый быстрый путь. Можно не начинать с интерфейсов, они возникают сами из утиной типизации. Это делает гексагоналку естественной, но нет защиты от
nilи мало компиляторных гарантий на уровне инвариантов (Option/Result-стиля), поэтому гексагоналка держится больше на дисциплине .Rust: Идеал. Трейты это идеальные порты, типы надежны.
C#: Комфорт. Ритуалов (интерфейсов и DI) чуть больше, чем в Go, но современные фичи (Records).
Java: Бюрократия. Чтобы сделать чисто, нужно продираться сквозь множество конфигов, плагинов и XML. Дорого, многословно и часто больно. Но в Kotlin получше уже.
Остальные (динамические): Избыточно. Усилий много, а защиты (гарантий) никакой. Там лучше работают простые подходы.
Если реализовывать, например, веб сервис на любом из языков тебе всё равно придётся взять какой-то фреймворк/библиотеку, чтобы не писать всё с нуля. И если ты будешь писать код так, что он везде будет использовать фичи этого фреймворка и ещё десятка зависимостей, то проект станет полностью "зависимым". Идея же разделить код так, чтобы core часть была независима от фреймворка.
Автор перемудрил, конечно.
Без enforce-плагина кто-то добавит импорт из infrastructure в domain. Без фабрики для даты тесты станут флаки. Без модулей границы размоются.
Именно на Java это сложно.
А в соседний стек стоит иногда заглядывать
Я не встречал таких трудностей с гексагоналкой в своих проектах на своём стеке.
Вот честно, я очень пытался всерьез воспринимать ваш "Магнум опус", но на совете отказаться от использования ломбока сдался.
Пускай я отхвачу минусов, но по-моему, вы сами себя убедили, что все понимаете. Ваши рассуждения и качество кода отчётливо выдают неприглядную правду об отсутствии достаточного практического опыта в вещах, о которых вы говорите.
В большинстве случаев мы не пишем объекты с инкапсуляцией, обычно у нас отдельно лежит объект от самой логики, потому что она выносится в какой‑нибудь отдельный синглтон класс. И получается, что объекты это просто контейнеры. Для такого удобно использовать record class. По привычке в проектах не используют public поля, а добавляют не думая геттеры и сеттеры с помощью аннотаций lombok, которые от public полей не отличаются. Для объекта с public полями, если нужно скрыть какое‑то поле, то ты меняешь модификатор и уже дизайниш метод доступа как тебе надо. А в сложных объектах всё пишется руками.
В реализации jpa сущностей я хотел это наглядно показать, но micronaut и mapstruct ломались и я забил на это.
Да у ломбока есть удобные аннотации, например, @Builder, @Slf4j. Если можно найти альтернативы @Builder, то альтернативы @Slf4j нет.
Когда я это написал больше думал о тех, кто необдуманно использует их и отдельно выделил в рекомендацию.
Спасибо за статью. В отличии от подобных статей у вас описаны рассуждения почему вы принимаете те или иные решения.

Реализация гексагональной архитектуры на Java