Что такое гексагональная архитектура?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Что такое гексагональная архитектура?
Определение
Гексагональная архитектура (Hexagonal Architecture), также известная как Ports & Adapters, — это архитектурный паттерн, предложенный Alistair Cockburn, который изолирует бизнес-логику от технических деталей.
Основа: изолировать бизнес-логику (домен) от внешних сервисов через Ports (интерфейсы) и Adapters (реализации).
Структура гексагональной архитектуры
┌─────────────────────────────────────────────────┐
│ Внешний мир (2/3 части) │
│ REST API │ SOAP │ WebSocket │ Database │
└──────────┬──────────────────────────────────────┘
│ Adapter
┌──────────▼──────────────────────────────────────┐
│ PORT (Interface) │
│ UserRepository, PaymentProcessor, EventBus │
└──────────┬──────────────────────────────────────┘
│ Implements
┌──────────▼──────────────────────────────────────┐
│ HEXAGON (Core/Domain) │
│ User Entities & Use Cases │
│ - CreateUserUseCase │
│ - UpdateUserUseCase │
│ - GetUserUseCase │
└──────────┬──────────────────────────────────────┘
│ Implements
┌──────────▼──────────────────────────────────────┐
│ PORT (Interface) │
│ Notification, Logger, Cache │
└──────────┬──────────────────────────────────────┘
│ Adapter
└──────────▼──────────────────────────────────────┘
Внешний мир (1/3 части)
Notification Service, Cache Service
Ключевые концепции
1. HEXAGON (Core/Domain)
Это сердце приложения — бизнес-логика, полностью независимая от внешних сервисов.
// Domain Entity
public class User {
private String id;
private String email;
private String name;
private boolean active;
public User(String id, String email, String name) {
this.id = id;
this.email = email;
this.name = name;
this.active = true;
}
public void deactivate() {
this.active = false;
}
}
// Use Case (бизнес-логика)
public class CreateUserUseCase {
private final UserRepository repository;
private final EmailValidator validator;
private final EventPublisher publisher;
public CreateUserUseCase(UserRepository repository,
EmailValidator validator,
EventPublisher publisher) {
this.repository = repository;
this.validator = validator;
this.publisher = publisher;
}
public User execute(CreateUserCommand cmd) throws ValidationException {
// Валидация
if (!validator.isValid(cmd.getEmail())) {
throw new ValidationException("Invalid email");
}
// Создание
User user = new User(UUID.randomUUID().toString(),
cmd.getEmail(),
cmd.getName());
// Сохранение (через port)
repository.save(user);
// Уведомление (через port)
publisher.publish(new UserCreatedEvent(user.getId()));
return user;
}
}
2. PORT (Interface/Contract)
Порт определяет контракт между доменом и внешним миром. Это интерфейсы, которые доменный код вызывает.
// INPUT PORT (Driven Port)
// Интерфейсы, которые домен ИСПОЛЬЗУЕТ
public interface UserRepository {
void save(User user);
Optional<User> findById(String id);
void delete(String id);
}
public interface EmailValidator {
boolean isValid(String email);
}
public interface EventPublisher {
void publish(DomainEvent event);
}
// OUTPUT PORT (Driving Port)
// Интерфейсы, которые ИСПОЛЬЗУЮТ домен
public interface CreateUserPort {
User createUser(CreateUserRequest request);
}
public interface GetUserPort {
User getUser(String userId);
}
3. ADAPTER (Implementation)
Адаптер реализует PORT, подключая конкретную технологию (БД, REST API, etc).
// PRIMARY ADAPTER (REST)
@RestController
@RequestMapping("/api/users")
public class CreateUserAdapter implements CreateUserPort {
private final CreateUserUseCase useCase;
public CreateUserAdapter(CreateUserUseCase useCase) {
this.useCase = useCase;
}
@PostMapping
public ResponseEntity<UserResponse> create(@RequestBody CreateUserRequest request) {
User user = useCase.execute(new CreateUserCommand(request));
return ResponseEntity.ok(new UserResponse(user));
}
}
// SECONDARY ADAPTER (PostgreSQL)
@Repository
public class PostgresUserRepository implements UserRepository {
private final JdbcTemplate jdbc;
@Override
public void save(User user) {
String sql = "INSERT INTO users (id, email, name, active) VALUES (?, ?, ?, ?)";
jdbc.update(sql, user.getId(), user.getEmail(), user.getName(), user.isActive());
}
@Override
public Optional<User> findById(String id) {
String sql = "SELECT * FROM users WHERE id = ?";
return jdbc.queryForObject(sql, (rs, rowNum) ->
new User(rs.getString("id"), rs.getString("email"), rs.getString("name")),
id
).map(Optional::of).orElse(Optional.empty());
}
}
// SECONDARY ADAPTER (Email Validator)
@Component
public class RegexEmailValidator implements EmailValidator {
private static final String EMAIL_PATTERN =
"^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
@Override
public boolean isValid(String email) {
return email.matches(EMAIL_PATTERN);
}
}
// SECONDARY ADAPTER (Event Publisher)
@Component
public class KafkaEventPublisher implements EventPublisher {
private final KafkaTemplate<String, String> kafka;
@Override
public void publish(DomainEvent event) {
kafka.send("domain-events", JsonUtils.toJson(event));
}
}
Полный пример: Order Service
// DOMAIN
public class Order {
private String id;
private String customerId;
private List<OrderItem> items;
private OrderStatus status;
public Order(String id, String customerId) {
this.id = id;
this.customerId = customerId;
this.items = new ArrayList<>();
this.status = OrderStatus.PENDING;
}
public void addItem(OrderItem item) {
items.add(item);
}
public void confirm() throws OrderException {
if (items.isEmpty()) {
throw new OrderException("Cannot confirm empty order");
}
this.status = OrderStatus.CONFIRMED;
}
}
// USE CASE
public class ConfirmOrderUseCase {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentProcessor paymentProcessor;
private final OrderEventPublisher eventPublisher;
public Order execute(String orderId) throws OrderException {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("Order not found"));
// Проверка наличия товара
for (OrderItem item : order.getItems()) {
if (!inventoryService.checkAvailable(item.getProductId(), item.getQuantity())) {
throw new OutOfStockException(item.getProductId());
}
}
// Подтверждение заказа
order.confirm();
// Резервирование товара
order.getItems().forEach(item ->
inventoryService.reserve(item.getProductId(), item.getQuantity())
);
// Обработка платежа
paymentProcessor.charge(order.getCustomerId(), order.getTotalPrice());
// Сохранение
orderRepository.save(order);
// Публикация события
eventPublisher.publish(new OrderConfirmedEvent(order.getId()));
return order;
}
}
// PORTS
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(String id);
}
public interface InventoryService {
boolean checkAvailable(String productId, int quantity);
void reserve(String productId, int quantity);
}
public interface PaymentProcessor {
void charge(String customerId, double amount) throws PaymentException;
}
public interface OrderEventPublisher {
void publish(DomainEvent event);
}
// ADAPTERS
@RestController
@RequestMapping("/api/orders")
public class OrderRestAdapter {
private final ConfirmOrderUseCase useCase;
@PostMapping("/{id}/confirm")
public ResponseEntity<OrderResponse> confirm(@PathVariable String id) {
Order order = useCase.execute(id);
return ResponseEntity.ok(new OrderResponse(order));
}
}
@Repository
public class JpaOrderRepository implements OrderRepository {
private final OrderJpaRepository jpa;
@Override
public void save(Order order) {
jpa.save(new OrderEntity(order));
}
@Override
public Optional<Order> findById(String id) {
return jpa.findById(id).map(OrderEntity::toDomain);
}
}
@Service
public class RemoteInventoryAdapter implements InventoryService {
private final RestTemplate rest;
@Override
public boolean checkAvailable(String productId, int quantity) {
InventoryResponse response = rest.getForObject(
"/inventory/" + productId, InventoryResponse.class
);
return response.getQuantity() >= quantity;
}
@Override
public void reserve(String productId, int quantity) {
rest.postForObject(
"/inventory/" + productId + "/reserve",
new ReserveRequest(quantity),
Void.class
);
}
}
Преимущества гексагональной архитектуры
- Тестируемость — можно подменять адаптеры на mock'и
@Test
public void testCreateOrder() {
// Mock adapters
OrderRepository mockRepo = mock(OrderRepository.class);
InventoryService mockInventory = mock(InventoryService.class);
// Инжектируем в use case
ConfirmOrderUseCase useCase = new ConfirmOrderUseCase(
mockRepo, mockInventory, /* ... */
);
// Тест работает с чистой доменной логикой
Order order = useCase.execute("123");
}
-
Независимость от фреймворков — домен не знает о Spring, JPA и т.д.
-
Легкость подмены технологий — хотим другую БД? Просто новый адаптер
-
Четкое разделение ответственности — каждый слой имеет одну роль
-
Дизайн driven по портам — интерфейсы определяют что нужно домену
Недостатки
- Больше code — много интерфейсов и адаптеров
- Complexity — может быть избыточна для маленьких приложений
- Learning curve — требует понимания архитектурных паттернов
Best Practices
- Домен не зависит от фреймворков
- Use Cases содержат бизнес-логику, не технические детали
- Ports определяют контракт, не реализацию
- Несколько адаптеров на один port - нормально (REST и gRPC на одну use case)
- Тестируй use cases с mock адаптерами