Обновить

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

Гексагональная архитектура это конечно хорошо, но вы забыли упомянуть об количестве кода, который порождается таким подходом, особенно количество 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 нет.

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

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

Хорошая статья! Но не всегда думаю стоит разносить по отдельным модулям (если это не оправдвно только). Сам пару лет назад попробовал гексагоналку и теперь практически любой новый проект пишу на ней, после заходишь через год в проект и сразу же видишь что где и как работает !
Ну да немного больше кода, маппинг между контроллером в домен, из домена в чущность - и обратно. Но эти затраты очень "окупаются" после! К тому же контроллеры чистые (в три строки), логика сразу понятна и отделена в useCases (без зависимостей, бины нужные создаются в конфигах), в домене все по DDD только там строится обьект и нигде иначе.
И я разбиваю чуть иначе (но смысл тот же):

├── adapters/
│ ├── in/
│ │ ├── consumer/
│ │ ├── controller/
│ │ │ ├── mapper/
│ │ │ ├── request/
│ │ │ ├── response/
│ │ │ └── Controller*.java
│ │ └── scheduler/
│ └── out/
│ ├── client/
│ ├── geocoder/
│ ├── repository/
│ │ ├── mapper/
│ │ ├── entity/
│ ├── AdapterPersistence*.java
├── application/
│ ├── ports/
│ │ ├── in/
│ │ │ ├── command/
│ │ │ ├── Job*.java
│ │ │ ├── Create*.java
│ │ │ ├── Get*.java
│ │ └── out/
│ │ ├── exception/
│ │ ├── Job*.java
│ └── usecase/
├── config/
├── core/
│ ├── domain/
│ ├── enums/
│ ├── exception/
│ └── factory/
└── Application

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

Публикации