Обновить

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

Гексагональная архитектура это конечно хорошо, но вы забыли упомянуть об количестве кода, который порождается таким подходом, особенно количество pojo/dto/model классов, т.е. просто пустых классов с полями:

-сама модель

-value object для модификации объекта модели

-дто для rest-интерфейсов

-jpa-entity для бд

Для каждого перехода с одного типа на другой потребуются свои мапперы, сервисы и т.д.

И все это на 90% случаев ради того, чтобы сохранить json в бд. Гексагональную архитектуру следует применять только для того подмножества доменной области, которая сдержит в себе непрерывные процессы моделирования, то есть некоторые объекты, которые живут в памяти со старта приложения, крутясь в цикле, например - серверная часть мультиплеерных игр.

Если же у вас request/response схема работы с данными, то лучше даже не влезать в гексагональную архитектуру, или влезать, но моделью данных сделать непосредственно jpa-entity

Работать с 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 нет.

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации