Что такое гексагональная архитектура (Hexagonal Architecture)?

Ответ

Гексагональная архитектура (Порты и адаптеры) — это архитектурный паттерн, который изолирует бизнес-логику (ядро приложения) от внешних зависимостей (БД, UI, внешние API), делая её независимой и легко тестируемой.

Ключевые компоненты:

  1. Ядро (Domain): Содержит чистую бизнес-логику и модели. Ничего не знает о внешнем мире.
  2. Порты (Ports): Интерфейсы, которые определяют как ядро может взаимодействовать с внешним миром.
    • Входящие порты (Primary/Driving Ports): Для случаев, когда ядро вызывают (например, UserService).
    • Исходящие порты (Secondary/Driven Ports): Для случаев, когда ядро обращается вовне (например, UserRepository).
  3. Адаптеры (Adapters): Реализации портов для конкретных технологий.
    • Входящие адаптеры: Контроллеры REST API, CLI, Web-формы.
    • Исходящие адаптеры: Реализации репозиториев для JPA, вызовы внешних REST-сервисов.

Схема потока данных: Внешний мир -> [Входящий адаптер] -> [Входящий порт] -> [Ядро] -> [Исходящий порт] -> [Исходящий адаптер] -> Внешний мир

Пример структуры:

// 1. ЯДРО: Доменный объект
public class User {
    private Long id;
    private String name;
}

// 2. ЯДРО: Исходящий порт (интерфейс репозитория)
public interface UserRepositoryPort {
    User findById(Long id);
}

// 3. ЯДРО: Входящий порт (интерфейс сервиса)
public interface UserServicePort {
    User getUser(Long id);
}

// 4. ЯДРО: Реализация бизнес-логики
@Service
public class UserService implements UserServicePort {
    private final UserRepositoryPort repository; // Зависимость от порта!
    public User getUser(Long id) { return repository.findById(id); }
}

// 5. АДАПТЕР: JPA-реализация исходящего порта
@Repository
public class JpaUserRepositoryAdapter implements UserRepositoryPort {
    @PersistenceContext
    private EntityManager em;
    public User findById(Long id) { return em.find(User.class, id); }
}

// 6. АДАПТЕР: REST-контроллер (входящий адаптер)
@RestController
public class UserController {
    private final UserServicePort userService; // Зависимость от порта!
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) { return userService.getUser(id); }
}

Главное преимущество: Технологии становятся заменяемыми деталями. Чтобы сменить БД с PostgreSQL на MongoDB, нужно лишь написать новый MongoUserRepositoryAdapter, не трогая ядро.

Ответ 18+ 🔞

Ну вот, сидишь ты такой, пишешь свой очередной микросервис, а там уже всё в говне — контроллеры к репозиториям прикручены намертво, тесты не запустить без поднятой базы, и в голове одна мысль: «Пиздец, опять». А ведь есть способ не сойти с ума, и зовётся он Гексагональная архитектура, или, для своих, «Порты и адаптеры». Суть её проще пареной репы, если не усложнять.

В чём соль, блядь? Всё построено на том, чтобы отделить святое — твою бизнес-логику — от всего остального грешного мира: баз данных, веб-морд, внешних API и прочей хуйни**. Ядро приложения должно жить в вакууме, в идеальной чистоте, и нихуя не знать о том, как там снаружи устроено.

Из чего это говно собрано:

  1. Ядро (Domain): Это святая святых, мозги проекта. Тут живут твои модели, правила, вся логика. Никаких @Entity, @RestController или вызовов к сторонним сервисам! Чистая, незамутнённая математика, блядь.
  2. Порты (Ports): Это такие дырки в стене твоего ядра, через которые оно может общаться с внешним миром, но не напрямую, а через интерфейсы. Их два вида:
    • Входящие порты: Когда мир хочет попросить ядро что-то сделать. Типа: «Эй, сервис, дай-ка пользователя!». Это интерфейсы твоих сервисов.
    • Исходящие порты: Когда ядру самому надо сходить куда-то наружу. Например: «Надо бы пользователя из базы достать». Это интерфейсы твоих репозиториев или клиентов к внешним API.
  3. Адаптеры (Adapters): А вот это уже та самая грязная работёнка. Это конкретные реализации этих портов для каждой технологии.
    • Входящий адаптер: REST-контроллер, который принимает HTTP-запрос, преобразует его в вызов метода твоего сервиса (порта) и отдаёт ответ. Или консольная команда, или обработчик сообщения из очереди.
    • Исходящий адаптер: Реализация репозитория на JPA, или на JDBC, или на MongoDB. Или клиент, который дергает чужой API.

Как это всё ебётся вместе, внатуре? Вот схема, проще которой только бутерброд с колбасой: Внешний запрос -> [Входящий адаптер (Контроллер)] -> [Входящий порт (Сервис)] -> [Ядро] -> [Исходящий порт (Репозиторий)] -> [Исходящий адаптер (JPA)] -> База данных

Смотри, как это выглядит в коде, на примере простого говна:

// 1. ЯДРО: Доменный объект. Никаких аннотаций JPA, чистое POJO.
public class User {
    private Long id;
    private String name;
    // Геттеры/сеттеры, логика...
}

// 2. ЯДРО: Исходящий порт. Интерфейс, который говорит "я умею искать юзера".
// Ядро знает только об этом интерфейсе. Ему похуй, как он реализован.
public interface UserRepositoryPort {
    User findById(Long id);
}

// 3. ЯДРО: Входящий порт. Интерфейс сервиса, который ядро предоставляет миру.
public interface UserServicePort {
    User getUser(Long id);
}

// 4. ЯДРО: Сама реализация бизнес-логики. Зависит ТОЛЬКО от портов!
@Service
public class UserService implements UserServicePort {
    private final UserRepositoryPort repository; // Зависимость от интерфейса, а не от JPA!
    public User getUser(Long id) {
        // Тут могла бы быть сложная логика, но её нет, потому что пример.
        return repository.findById(id);
    }
}

// 5. АДАПТЕР: JPA-реализация исходящего порта. Вот тут уже начинается грязь.
@Repository
public class JpaUserRepositoryAdapter implements UserRepositoryPort {
    @PersistenceContext
    private EntityManager em;
    public User findById(Long id) {
        return em.find(User.class, id); // Конкретная работа с JPA
    }
}

// 6. АДАПТЕР: REST-контроллер (входящий адаптер). Принимает HTTP, дергает порт.
@RestController
public class UserController {
    private final UserServicePort userService; // Опять зависим от порта!
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUser(id);
    }
}

А в чём, собственно, профит, ёпта? А профит в том, что технологии становятся заменяемыми деталями, как батарейки в пульте. Захотел поменять PostgreSQL на MongoDB? Пиздец проблема? Да ни хуя! Пишешь новый MongoUserRepositoryAdapter, который имплементит тот же UserRepositoryPort, подменяешь бин в конфиге — и ядро, твоя святая бизнес-логика, даже не почует подвоха. Оно как работало, так и работает. Тестировать ядро можно в полной изоляции, подсовывая ему заглушки вместо адаптеров. Красота, а не жизнь!