Что такое гексагональная архитектура (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 с интерфейсами», упуская суть направления зависимостей.