Gymterview
middle

Что такое гексагональная архитектура (Ports and Adapters)

Гексагональная архитектура (Hexagonal Architecture), также известная как Ports and Adapters, — это архитектурный стиль, предложенный Алистером Кокберном, основная идея которого — изолировать ядро приложения (бизнес-логику) от внешнего мира с помощью портов (интерфейсов) и адаптеров (реализаций). Представьте розетку и вилку: розетка (порт) задаёт стандарт, а вилка (адаптер) может быть любой — главное, чтобы она подходила к розетке.

Структура

Пример
                    ┌──── Адаптеры (Driving / Входящие) ────┐
                    │                                        │
              ┌───────────┐                          ┌───────────┐
              │  REST API │                          │   CLI     │
              │ Controller│                          │  Adapter  │
              └─────┬─────┘                          └─────┬─────┘
                    │                                      │
                    ▼                                      ▼
              ┌─────────────────────────────────────────────────┐
              │              Входящие порты                     │
              │         (интерфейсы Use Case)                   │
              │  ┌─────────────────────────────────────────┐    │
              │  │                                         │    │
              │  │         Ядро приложения                  │    │
              │  │     (Domain Model + Use Cases)          │    │
              │  │                                         │    │
              │  └─────────────────────────────────────────┘    │
              │              Исходящие порты                    │
              │         (интерфейсы репозиториев,               │
              │          внешних сервисов)                      │
              └──────────────┬──────────────┬───────────────────┘
                             │              │
                             ▼              ▼
                       ┌──────────┐  ┌────────────┐
                       │ JPA      │  │  Kafka     │
                       │ Adapter  │  │  Adapter   │
                       └──────────┘  └────────────┘
                    └──── Адаптеры (Driven / Исходящие) ────┘

Порты и адаптеры

Порт — это интерфейс, определяющий контракт взаимодействия:

  • Входящий (driving) порт — интерфейс, через который внешний мир обращается к приложению (Use Case).
  • Исходящий (driven) порт — интерфейс, через который приложение обращается к внешнему миру (БД, очередь сообщений, внешний API).

Адаптер — конкретная реализация порта:

  • Входящий адаптер — REST-контроллер, gRPC-сервис, обработчик очереди.
  • Исходящий адаптер — JPA-репозиторий, HTTP-клиент, Kafka-продюсер.

Пример на Java

Пример кода
// Входящий порт (Use Case)
public interface TransferMoneyUseCase {
    void transfer(TransferCommand command);
}

// Доменная модель
public class Account {
    private AccountId id;
    private Money balance;

    public void withdraw(Money amount) {
        if (balance.isLessThan(amount)) {
            throw new InsufficientFundsException(id, amount);
        }
        balance = balance.minus(amount);
    }

    public void deposit(Money amount) {
        balance = balance.plus(amount);
    }
}

// Исходящий порт
public interface AccountRepository {
    Account findById(AccountId id);
    void save(Account account);
}

// Реализация Use Case (Application Service)
@Service
public class TransferMoneyService implements TransferMoneyUseCase {

    private final AccountRepository accountRepository;

    public TransferMoneyService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    @Override
    @Transactional
    public void transfer(TransferCommand command) {
        Account from = accountRepository.findById(command.getSourceId());
        Account to = accountRepository.findById(command.getTargetId());
        from.withdraw(command.getAmount());
        to.deposit(command.getAmount());
        accountRepository.save(from);
        accountRepository.save(to);
    }
}

// Входящий адаптер (REST)
@RestController
public class AccountController {

    private final TransferMoneyUseCase transferMoney;

    @PostMapping("/transfer")
    public ResponseEntity<Void> transfer(@RequestBody TransferRequest request) {
        transferMoney.transfer(new TransferCommand(
            new AccountId(request.getFromId()),
            new AccountId(request.getToId()),
            Money.of(request.getAmount())
        ));
        return ResponseEntity.ok().build();
    }
}

// Исходящий адаптер (JPA)
@Component
public class JpaAccountRepository implements AccountRepository {

    private final AccountJpaRepository jpaRepository;

    @Override
    public Account findById(AccountId id) {
        AccountEntity entity = jpaRepository.findById(id.getValue())
            .orElseThrow(() -> new AccountNotFoundException(id));
        return AccountMapper.toDomain(entity);
    }

    @Override
    public void save(Account account) {
        jpaRepository.save(AccountMapper.toEntity(account));
    }
}

Плюсы

  • Бизнес-логика полностью изолирована от инфраструктуры.
  • Легко подменять адаптеры (например, заменить PostgreSQL на MongoDB).
  • Отличная тестируемость — можно тестировать ядро без БД и сети.
  • Соответствует принципу инверсии зависимостей (DIP из SOLID).

Минусы

  • Больше кода (интерфейсы, маппинг между слоями).
  • Сложнее для небольших CRUD-приложений.
  • Требует дисциплины от команды.

На собеседовании: Ключевое, что хотят услышать — это разницу между входящими и исходящими портами и понимание инверсии зависимостей. Частая ошибка — описывать гексагональную архитектуру как «просто layered с интерфейсами», упуская суть направления зависимостей.