六边形架构:端口与适配器
六边形架构:端口与适配器
六边形架构概述
六边形架构(Hexagonal Architecture),也称为端口与适配器架构(Ports and Adapters),由Alistair Cockburn提出。其核心思想是将应用程序的核心业务逻辑与外部依赖(如数据库、UI、消息队列等)通过端口和适配器进行解耦。
核心概念
端口(Ports)
端口是应用程序与外部世界交互的接口点。端口定义了应用程序需要什么服务以及提供什么服务。
// 入站端口 - 应用程序提供的服务
public interface OrderService {
Order createOrder(CreateOrderCommand command);
Order getOrder(Long id);
void cancelOrder(Long id);
}
// 出站端口 - 应用程序需要的服务
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(Long id);
void delete(Order order);
}
public interface PaymentGateway {
PaymentResult processPayment(PaymentRequest request);
RefundResult processRefund(RefundRequest request);
}
public interface NotificationService {
void sendOrderConfirmation(Order order);
void sendOrderCancellation(Order order);
}
适配器(Adapters)
适配器是端口的具体实现,负责将外部系统与端口连接起来。
// 入站适配器 - REST API
@RestController
@RequestMapping("/api/orders")
public class OrderRestAdapter {
private final OrderService orderService;
private final OrderMapper mapper;
public OrderRestAdapter(OrderService orderService, OrderMapper mapper) {
this.orderService = orderService;
this.mapper = mapper;
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
CreateOrderCommand command = mapper.toCommand(request);
Order order = orderService.createOrder(command);
OrderResponse response = mapper.toResponse(order);
return ResponseEntity.ok(response);
}
@GetMapping("/{id}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
Order order = orderService.getOrder(id);
OrderResponse response = mapper.toResponse(order);
return ResponseEntity.ok(response);
}
}
// 入站适配器 - 消息队列消费者
@Component
public class OrderMessageAdapter {
private final OrderService orderService;
private final OrderMapper mapper;
@KafkaListener(topics = "order-commands")
public void handleOrderCommand(String message) {
CreateOrderCommand command = mapper.fromJson(message);
orderService.createOrder(command);
}
}
// 出站适配器 - JPA实现
@Repository
public class JpaOrderAdapter implements OrderRepository {
private final OrderJpaRepository jpaRepository;
private final OrderMapper mapper;
@Override
public Order save(Order order) {
OrderJpaEntity entity = mapper.toJpaEntity(order);
OrderJpaEntity savedEntity = jpaRepository.save(entity);
return mapper.toDomainEntity(savedEntity);
}
@Override
public Optional<Order> findById(Long id) {
return jpaRepository.findById(id)
.map(mapper::toDomainEntity);
}
}
// 出站适配器 - 外部支付网关
@Component
public class StripePaymentAdapter implements PaymentGateway {
private final StripeClient stripeClient;
@Override
public PaymentResult processPayment(PaymentRequest request) {
try {
Charge charge = stripeClient.charges().create(
ChargeCreateParams.builder()
.setAmount(request.getAmount().longValue())
.setCurrency("usd")
.setSource(request.getPaymentMethod())
.build()
);
return PaymentResult.success(charge.getId());
} catch (StripeException e) {
return PaymentResult.failure(e.getMessage());
}
}
}
架构图
┌─────────────────────────────────────────────────────────┐
│ 入站适配器 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ REST API │ │ 消息队列 │ │ CLI │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 入站端口 (Ports) │ │
│ │ OrderService, CommandHandler │ │
│ └─────────────────────┬───────────────────────┘ │
│ │ │
│ ┌─────────────────────┴───────────────────────┐ │
│ │ 核心业务逻辑 │ │
│ │ 领域模型、业务规则、用例 │ │
│ └─────────────────────┬───────────────────────┘ │
│ │ │
│ ┌─────────────────────┴───────────────────────┐ │
│ │ 出站端口 (Ports) │ │
│ │ Repository, PaymentGateway, etc. │ │
│ └─────────────────────┬───────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ JPA │ │ Stripe │ │ Email │ │
│ │ Adapter │ │ Adapter │ │ Adapter │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ 出站适配器 │
└─────────────────────────────────────────────────────────┘
业务逻辑实现
核心业务逻辑不依赖任何外部框架或技术:
// 领域实体
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderItem> items;
private OrderStatus status;
private Money totalAmount;
private LocalDateTime createdAt;
// 业务规则
public static Order create(CustomerId customerId, List<OrderItem> items) {
if (items.isEmpty()) {
throw new DomainException("Order must contain at least one item");
}
Order order = new Order();
order.id = OrderId.generate();
order.customerId = customerId;
order.items = new ArrayList<>(items);
order.status = OrderStatus.CREATED;
order.createdAt = LocalDateTime.now();
order.recalculateTotal();
return order;
}
public void cancel() {
if (status != OrderStatus.CREATED) {
throw new DomainException("Only created orders can be cancelled");
}
this.status = OrderStatus.CANCELLED;
}
public void confirm() {
if (status != OrderStatus.CREATED) {
throw new DomainException("Only created orders can be confirmed");
}
this.status = OrderStatus.CONFIRMED;
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getTotalPrice)
.reduce(Money.ZERO, Money::add);
}
}
// 用例实现
@Service
public class CreateOrderUseCase {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final NotificationService notificationService;
public CreateOrderUseCase(
OrderRepository orderRepository,
PaymentGateway paymentGateway,
NotificationService notificationService) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
this.notificationService = notificationService;
}
@Transactional
public Order execute(CreateOrderCommand command) {
// 1. 创建订单
Order order = Order.create(
new CustomerId(command.getCustomerId()),
command.getItems().stream()
.map(this::toOrderItem)
.collect(Collectors.toList())
);
// 2. 处理支付
PaymentRequest paymentRequest = new PaymentRequest(
order.getId(),
order.getTotalAmount(),
command.getPaymentMethod()
);
PaymentResult paymentResult = paymentGateway.processPayment(paymentRequest);
if (!paymentResult.isSuccess()) {
throw new PaymentException("Payment failed: " + paymentResult.getErrorMessage());
}
// 3. 确认订单
order.confirm();
// 4. 保存订单
Order savedOrder = orderRepository.save(order);
// 5. 发送通知
notificationService.sendOrderConfirmation(savedOrder);
return savedOrder;
}
}
测试策略
六边形架构使得测试变得简单,因为业务逻辑与外部依赖完全解耦:
// 单元测试 - 使用内存适配器
class CreateOrderUseCaseTest {
private CreateOrderUseCase useCase;
private InMemoryOrderRepository orderRepository;
private InMemoryPaymentGateway paymentGateway;
private InMemoryNotificationService notificationService;
@BeforeEach
void setUp() {
orderRepository = new InMemoryOrderRepository();
paymentGateway = new InMemoryPaymentGateway();
notificationService = new InMemoryNotificationService();
useCase = new CreateOrderUseCase(orderRepository, paymentGateway, notificationService);
}
@Test
void shouldCreateOrderSuccessfully() {
// Given
CreateOrderCommand command = new CreateOrderCommand(
1L,
List.of(new OrderItemCommand(100L, 2)),
"credit_card"
);
paymentGateway.mockSuccess();
// When
Order order = useCase.execute(command);
// Then
assertNotNull(order.getId());
assertEquals(OrderStatus.CONFIRMED, order.getStatus());
assertEquals(1, order.getItems().size());
// 验证订单已保存
Optional<Order> savedOrder = orderRepository.findById(order.getId());
assertTrue(savedOrder.isPresent());
// 验证支付已处理
assertEquals(1, paymentGateway.getProcessedPayments().size());
// 验证通知已发送
assertEquals(1, notificationService.getSentNotifications().size());
}
}
// 集成测试 - 使用真实适配器
@SpringBootTest
class OrderIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldCreateOrderViaRestApi() {
// Given
CreateOrderRequest request = new CreateOrderRequest(
1L,
List.of(new OrderItemRequest(100L, 2)),
"credit_card"
);
// When
ResponseEntity<OrderResponse> response = restTemplate.postForEntity(
"/api/orders",
request,
OrderResponse.class
);
// Then
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody().getId());
// 验证数据库
Optional<Order> order = orderRepository.findById(response.getBody().getId());
assertTrue(order.isPresent());
}
}
六边形架构的优点
- 技术无关性:业务逻辑不依赖特定技术栈
- 可测试性:可以使用内存适配器进行快速单元测试
- 可替换性:可以轻松替换外部依赖(如更换数据库或支付网关)
- 多入口支持:可以同时支持REST API、消息队列、CLI等多种入口
- 团队协作:不同团队可以并行开发不同的适配器
实施建议
- 定义清晰的端口:端口应该只包含业务需要的操作
- 保持核心纯净:核心业务逻辑不应包含任何框架注解或技术代码
- 使用依赖注入:通过构造函数注入适配器,便于测试和替换
- 为每个外部系统创建适配器:不要直接在业务逻辑中调用外部API
- 编写测试:使用内存适配器进行单元测试,使用真实适配器进行集成测试