引言:从分层架构的困境说起
在我参与过的多个微服务项目中,几乎无一例外地遇到了同样的维护难题:业务逻辑与技术框架的深度耦合。典型的Spring Boot项目往往呈现出这样的结构:
controller/ # HTTP接口层
service/ # 业务逻辑层
repository/ # 数据访问层
entity/ # JPA实体
这种传统的三层架构在项目初期确实高效,但随着业务复杂度的提升,问题逐渐显现:
-
技术锁定:业务逻辑与Spring、JPA等技术框架深度绑定
-
测试困难:单元测试需要启动Spring上下文,运行缓慢
-
变更成本高:替换数据库或消息队列需要大量修改业务代码
-
领域模型贫血:业务逻辑散落在Service中,领域对象仅作为数据载体
今天,我将通过一个真实的订单系统重构案例,深入讲解六边形架构(Hexagonal Architecture)如何解决这些问题,并展示其在实际微服务项目中的落地实践。
一、六边形架构核心思想
1.1 什么是六边形架构?
六边形架构由Alistair Cockburn提出,也被称为端口与适配器架构(Ports and Adapters)。其核心思想是:将业务逻辑置于中心,外部依赖(数据库、消息队列、UI等)通过端口与适配器进行交互。
[用户界面适配器]
↓
[外部系统] → [输入端口] → [应用层] → [领域层] → [输出端口] → [数据库适配器]
↑ ↓
[消息队列适配器] [外部API适配器]
1.2 核心原则
依赖倒置:所有依赖指向领域层,外部依赖依赖抽象接口
技术中立:业务逻辑不依赖任何外部框架或技术
可测试性:可以独立测试业务逻辑,无需基础设施
可替换性:可以轻松替换外部组件(如从MySQL切换到MongoDB)
二、案例背景:订单履约系统重构
我们以一个典型的订单履约系统为例,该系统包含以下功能:
-
创建订单
-
支付订单
-
发货处理
-
库存扣减
-
消息通知
技术栈:Spring Boot + JPA + Kafka + Redis
2.1 传统实现的痛点
// 传统实现的典型代码
@Service
@Transactional
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Order createOrder(CreateOrderRequest request) {
// 1. 参数校验
if (request.getAmount() <= 0) {
throw new BusinessException("金额必须大于0");
}
// 2. 创建订单实体
OrderEntity order = new OrderEntity();
order.setOrderNo(generateOrderNo());
order.setAmount(request.getAmount());
order.setStatus(OrderStatus.CREATED);
// 3. 保存到数据库
orderRepository.save(order);
// 4. 缓存订单信息
redisTemplate.opsForValue().set("order:" + order.getId(), order);
// 5. 发送消息
kafkaTemplate.send("order-created", order.getId());
return order;
}
}
这段代码的问题显而易见:
-
OrderService直接依赖KafkaTemplate、RedisTemplate,无法独立测试
-
更换消息中间件(如RocketMQ)需要修改业务代码
-
业务逻辑与基础设施代码混在一起
三、六边形架构重构实战
3.1 项目结构设计
order-service/
├── application/ # 应用层
│ ├── port/ # 端口接口
│ │ ├── input/ # 输入端口
│ │ └── output/ # 输出端口
│ └── service/ # 应用服务
├── domain/ # 领域层
│ ├── model/ # 领域模型
│ ├── service/ # 领域服务
│ └── event/ # 领域事件
├── infrastructure/ # 基础设施
│ ├── adapter/ # 适配器实现
│ │ ├── input/ # 输入适配器
│ │ └── output/ # 输出适配器
│ └── config/ # 配置类
└── start/ # 启动模块
3.2 领域层(核心)
首先,定义纯净的领域模型,不包含任何框架注解:
// domain/model/Order.java
package com.example.order.domain.model;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 订单聚合根 – 纯粹的领域对象
* 不含任何框架注解,只有业务逻辑
*/
public class Order {
private OrderId id;
private String orderNo;
private UserId userId;
private Money totalAmount;
private OrderStatus status;
private List<OrderItem> items;
private LocalDateTime createdAt;
private LocalDateTime paidAt;
// 私有构造器,通过工厂方法创建
private Order() {}
/**
* 创建订单(领域工厂方法)
*/
public static Order create(UserId userId, List<OrderItem> items) {
Order order = new Order();
order.id = OrderId.generate();
order.orderNo = OrderNoGenerator.generate();
order.userId = userId;
order.items = new ArrayList<>(items);
order.totalAmount = items.stream()
.map(OrderItem::getSubTotal)
.reduce(Money.ZERO, Money::add);
order.status = OrderStatus.CREATED;
order.createdAt = LocalDateTime.now();
// 注册领域事件
order.registerEvent(new OrderCreatedEvent(order.id, order.userId));
return order;
}
/**
* 支付订单(业务行为)
*/
public void pay(Money amount) {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("订单状态错误,不能支付");
}
if (!totalAmount.equals(amount)) {
throw new IllegalArgumentException("支付金额与订单金额不符");
}
this.status = OrderStatus.PAID;
this.paidAt = LocalDateTime.now();
// 注册领域事件
registerEvent(new OrderPaidEvent(this.id, this.userId));
}
/**
* 发货(业务行为)
*/
public void ship() {
if (status != OrderStatus.PAID) {
throw new IllegalStateException("只有已支付的订单才能发货");
}
this.status = OrderStatus.SHIPPED;
registerEvent(new OrderShippedEvent(this.id));
}
// 其他业务方法…
}
领域层的值对象:
// domain/model/Money.java
package com.example.order.domain.model;
import java.math.BigDecimal;
import java.util.Objects;
/**
* 金额值对象 – 不可变
*/
public final class Money {
private final BigDecimal amount;
private final String currency;
public static final Money ZERO = new Money(BigDecimal.ZERO, "CNY");
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币类型不匹配");
}
return new Money(this.amount.add(other.amount), this.currency);
}
// equals, hashCode, getters…
}
3.3 端口定义(应用层)
输入端口:定义业务用例接口
// application/port/input/OrderUseCase.java
package com.example.order.application.port.input;
import com.example.order.domain.model.Order;
import com.example.order.domain.model.OrderId;
import com.example.order.domain.model.UserId;
import java.util.List;
/**
* 输入端口:定义应用可以执行的用例
* 由应用服务实现,由适配器调用
*/
public interface OrderUseCase {
Order createOrder(CreateOrderCommand command);
void payOrder(PayOrderCommand command);
void shipOrder(OrderId orderId);
Order getOrder(OrderId orderId);
List<Order> getUserOrders(UserId userId);
// 命令对象(DTO)
record CreateOrderCommand(UserId userId, List<OrderItemCommand> items) {}
record OrderItemCommand(String productId, Integer quantity, Money price) {}
record PayOrderCommand(OrderId orderId, Money amount, PaymentMethod method) {}
}
输出端口:定义基础设施需要实现的接口
// application/port/output/OrderRepository.java
package com.example.order.application.port.output;
import com.example.order.domain.model.Order;
import com.example.order.domain.model.OrderId;
import com.example.order.domain.model.UserId;
import java.util.List;
import java.util.Optional;
/**
* 输出端口:持久化接口
* 由基础设施层实现,业务层依赖此接口
*/
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(OrderId id);
List<Order> findByUserId(UserId userId);
void delete(OrderId id);
boolean exists(OrderId id);
}
// application/port/output/EventPublisher.java
package com.example.order.application.port.output;
import com.example.order.domain.event.DomainEvent;
/**
* 输出端口:事件发布接口
*/
public interface EventPublisher {
void publish(DomainEvent event);
void publishAll(Iterable<DomainEvent> events);
}
// application/port/output/InventoryService.java
package com.example.order.application.port.output;
import com.example.order.domain.model.OrderItem;
import java.util.List;
/**
* 输出端口:库存服务接口
* 可能由HTTP客户端、gRPC客户端实现
*/
public interface InventoryPort {
boolean checkStock(List<OrderItem> items);
void deductStock(List<OrderItem> items);
void revertStock(List<OrderItem> items);
}
3.4 应用服务实现
// application/service/OrderApplicationService.java
package com.example.order.application.service;
import com.example.order.application.port.input.OrderUseCase;
import com.example.order.application.port.output.EventPublisher;
import com.example.order.application.port.output.InventoryPort;
import com.example.order.application.port.output.OrderRepository;
import com.example.order.domain.event.DomainEvent;
import com.example.order.domain.model.Order;
import com.example.order.domain.model.OrderItem;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 应用服务:编排领域对象,协调基础设施
* 不包含业务逻辑,只负责协调
*/
public class OrderApplicationService implements OrderUseCase {
private final OrderRepository orderRepository;
private final InventoryPort inventoryPort;
private final EventPublisher eventPublisher;
public OrderApplicationService(
OrderRepository orderRepository,
InventoryPort inventoryPort,
EventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.inventoryPort = inventoryPort;
this.eventPublisher = eventPublisher;
}
@Override
@Transactional
public Order createOrder(CreateOrderCommand command) {
// 1. 转换为领域对象
List<OrderItem> items = command.items().stream()
.map(item -> new OrderItem(
item.productId(),
item.quantity(),
item.price()))
.collect(Collectors.toList());
// 2. 调用领域工厂创建订单
Order order = Order.create(command.userId(), items);
// 3. 调用库存服务(通过端口)
if (!inventoryPort.checkStock(items)) {
throw new BusinessException("库存不足");
}
// 4. 保存订单
Order savedOrder = orderRepository.save(order);
// 5. 扣减库存
inventoryPort.deductStock(items);
// 6. 发布领域事件
eventPublisher.publishAll(order.getDomainEvents());
order.clearEvents();
return savedOrder;
}
@Override
@Transactional
public void payOrder(PayOrderCommand command) {
// 1. 获取订单
Order order = orderRepository.findById(command.orderId())
.orElseThrow(() -> new OrderNotFoundException(command.orderId()));
// 2. 执行支付业务逻辑(领域方法)
order.pay(command.amount());
// 3. 保存状态变更
orderRepository.save(order);
// 4. 发布事件
eventPublisher.publishAll(order.getDomainEvents());
order.clearEvents();
}
// 其他方法实现…
}
3.5 基础设施适配器实现
输入适配器:REST Controller
// infrastructure/adapter/input/web/OrderController.java
package com.example.order.infrastructure.adapter.input.web;
import com.example.order.application.port.input.OrderUseCase;
import com.example.order.domain.model.Order;
import com.example.order.domain.model.OrderId;
import com.example.order.domain.model.UserId;
import org.springframework.web.bind.annotation.*;
/**
* 输入适配器:REST API
* 将HTTP请求转换为应用层命令
*/
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderUseCase orderUseCase;
public OrderController(OrderUseCase orderUseCase) {
this.orderUseCase = orderUseCase;
}
@PostMapping
public OrderResponse createOrder(@RequestBody CreateOrderRequest request) {
// 1. 转换请求为命令
OrderUseCase.CreateOrderCommand command =
new OrderUseCase.CreateOrderCommand(
UserId.of(request.getUserId()),
request.getItems().stream()
.map(item -> new OrderUseCase.OrderItemCommand(
item.getProductId(),
item.getQuantity(),
new Money(item.getPrice(), "CNY")))
.toList()
);
// 2. 调用应用服务
Order order = orderUseCase.createOrder(command);
// 3. 转换为响应DTO
return OrderResponse.from(order);
}
@PostMapping("/{orderId}/pay")
public void payOrder(@PathVariable String orderId,
@RequestBody PayOrderRequest request) {
orderUseCase.payOrder(new OrderUseCase.PayOrderCommand(
OrderId.of(orderId),
new Money(request.getAmount(), "CNY"),
request.getMethod()
));
}
// 其他接口…
}
输出适配器:JPA实现
// infrastructure/adapter/output/persistence/OrderJpaRepository.java
package com.example.order.infrastructure.adapter.output.persistence;
import com.example.order.application.port.output.OrderRepository;
import com.example.order.domain.model.Order;
import com.example.order.domain.model.OrderId;
import com.example.order.domain.model.UserId;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 输出适配器:JPA实现
* 将领域对象转换为JPA实体,处理持久化细节
*/
@Repository
public class OrderJpaRepository implements OrderRepository {
private final SpringDataOrderRepository springRepository;
private final OrderMapper mapper;
public OrderJpaRepository(
SpringDataOrderRepository springRepository,
OrderMapper mapper) {
this.springRepository = springRepository;
this.mapper = mapper;
}
@Override
public Order save(Order order) {
OrderEntity entity = mapper.toEntity(order);
OrderEntity saved = springRepository.save(entity);
return mapper.toDomain(saved);
}
@Override
public Optional<Order> findById(OrderId id) {
return springRepository.findById(id.getValue())
.map(mapper::toDomain);
}
@Override
public List<Order> findByUserId(UserId userId) {
return springRepository.findByUserId(userId.getValue())
.stream()
.map(mapper::toDomain)
.collect(Collectors.toList());
}
// 其他方法…
}
// JPA实体(基础设施细节)
@Entity
@Table(name = "orders")
class OrderEntity {
@Id
private String id;
private String orderNo;
private String userId;
private BigDecimal totalAmount;
private String currency;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private LocalDateTime createdAt;
// getters, setters…
}
输出适配器:Kafka事件发布
// infrastructure/adapter/output/messaging/KafkaEventPublisher.java
package com.example.order.infrastructure.adapter.output.messaging;
import com.example.order.application.port.output.EventPublisher;
import com.example.order.domain.event.DomainEvent;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.kafka.core.KafkaTemplate;
/**
* 输出适配器:Kafka事件发布
* 将领域事件转换为Kafka消息
*/
@Component
public class KafkaEventPublisher implements EventPublisher {
private final KafkaTemplate<String, String> kafkaTemplate;
private final ObjectMapper objectMapper;
public KafkaEventPublisher(
KafkaTemplate<String, String> kafkaTemplate,
ObjectMapper objectMapper) {
this.kafkaTemplate = kafkaTemplate;
this.objectMapper = objectMapper;
}
@Override
public void publish(DomainEvent event) {
try {
// 将领域事件转换为JSON
String topic = resolveTopic(event);
String message = objectMapper.writeValueAsString(event);
// 发送到Kafka
kafkaTemplate.send(topic, message);
} catch (Exception e) {
throw new MessagingException("Failed to publish event", e);
}
}
private String resolveTopic(DomainEvent event) {
// 根据事件类型决定topic
if (event instanceof OrderCreatedEvent) {
return "order.created";
} else if (event instanceof OrderPaidEvent) {
return "order.paid";
}
return "order.events";
}
}
输出适配器:HTTP客户端调用库存服务
// infrastructure/adapter/output/http/InventoryHttpAdapter.java
package com.example.order.infrastructure.adapter.output.http;
import com.example.order.application.port.output.InventoryPort;
import com.example.order.domain.model.OrderItem;
import org.springframework.web.client.RestTemplate;
/**
* 输出适配器:HTTP调用库存服务
*/
@Component
public class InventoryHttpAdapter implements InventoryPort {
private final RestTemplate restTemplate;
private final String inventoryServiceUrl;
public InventoryHttpAdapter(
RestTemplate restTemplate,
@Value("${services.inventory.url}") String inventoryServiceUrl) {
this.restTemplate = restTemplate;
this.inventoryServiceUrl = inventoryServiceUrl;
}
@Override
public boolean checkStock(List<OrderItem> items) {
CheckStockRequest request = CheckStockRequest.from(items);
CheckStockResponse response = restTemplate.postForObject(
inventoryServiceUrl + "/api/stock/check",
request,
CheckStockResponse.class);
return response.isSuccess();
}
@Override
public void deductStock(List<OrderItem> items) {
DeductStockRequest request = DeductStockRequest.from(items);
restTemplate.postForObject(
inventoryServiceUrl + "/api/stock/deduct",
request,
Void.class);
}
// 其他方法…
}
3.6 依赖注入配置
// infrastructure/config/OrderServiceConfig.java
package com.example.order.infrastructure.config;
import com.example.order.application.port.output.EventPublisher;
import com.example.order.application.port.output.InventoryPort;
import com.example.order.application.port.output.OrderRepository;
import com.example.order.application.service.OrderApplicationService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置类:负责组装应用服务
* 清晰展示依赖关系
*/
@Configuration
public class OrderServiceConfig {
@Bean
public OrderApplicationService orderApplicationService(
OrderRepository orderRepository,
InventoryPort inventoryPort,
EventPublisher eventPublisher) {
return new OrderApplicationService(
orderRepository,
inventoryPort,
eventPublisher);
}
}
四、六边形架构的优势体现
4.1 极致的可测试性
我们可以完全脱离Spring,独立测试业务逻辑:
// test/application/OrderApplicationServiceTest.java
class OrderApplicationServiceTest {
private OrderApplicationService orderService;
private InMemoryOrderRepository orderRepository;
private MockInventoryPort inventoryPort;
private SpyEventPublisher eventPublisher;
@BeforeEach
void setUp() {
// 使用测试替身
orderRepository = new InMemoryOrderRepository();
inventoryPort = new MockInventoryPort();
eventPublisher = new SpyEventPublisher();
orderService = new OrderApplicationService(
orderRepository,
inventoryPort,
eventPublisher);
}
@Test
void shouldCreateOrderSuccessfully() {
// Given
var command = new OrderUseCase.CreateOrderCommand(
UserId.of("user123"),
List.of(new OrderUseCase.OrderItemCommand("product1", 2, Money.of(100))));
inventoryPort.setStockAvailable(true);
// When
Order order = orderService.createOrder(command);
// Then
assertNotNull(order);
assertEquals(OrderStatus.CREATED, order.getStatus());
assertTrue(orderRepository.findById(order.getId()).isPresent());
assertTrue(inventoryPort.wasDeductCalled());
assertEquals(1, eventPublisher.getPublishedEvents().size());
}
// 测试类实现…
static class InMemoryOrderRepository implements OrderRepository {
private Map<OrderId, Order> store = new HashMap<>();
@Override
public Order save(Order order) {
store.put(order.getId(), order);
return order;
}
// 其他方法…
}
}
4.2 技术栈可替换性
假设需要将Kafka替换为RocketMQ,只需实现新的适配器:
// infrastructure/adapter/output/messaging/RocketMQEventPublisher.java
@Component
public class RocketMQEventPublisher implements EventPublisher {
private final RocketMQTemplate rocketMQTemplate;
@Override
public void publish(DomainEvent event) {
rocketMQTemplate.syncSend(topic, event);
}
}
// 只需修改配置,业务代码零改动
@Configuration
public class MessagingConfig {
@Bean
@ConditionalOnProperty(name = "messaging.provider", havingValue = "rocketmq")
public EventPublisher rocketMQEventPublisher() {
return new RocketMQEventPublisher();
}
}
4.3 清晰的关注点分离
每个组件都有明确的职责:
| 领域层 | 业务规则、领域逻辑 | 无 |
| 应用层 | 用例编排、事务协调 | 依赖端口接口 |
| 输入适配器 | 接收外部请求 | 依赖应用层 |
| 输出适配器 | 技术实现细节 | 实现端口接口 |
4.4 微服务架构中的协同优势
在微服务架构中,六边形架构特别适合:
BFF模式:可以为不同客户端实现不同的输入适配器
// Web端适配器
@RestController
public class WebOrderController {
private final OrderUseCase orderUseCase;
// Web-specific conversion
}
// 移动端适配器
@RestController
@RequestMapping("/mobile")
public class MobileOrderController {
private final OrderUseCase orderUseCase;
// Mobile-specific conversion
}
事件驱动架构:可以同时支持多种消息协议
// Kafka适配器
@Component
public class OrderEventKafkaConsumer {
@KafkaListener(topics = "order-events")
public void consume(OrderEvent event) {
// 处理事件
}
}
// WebSocket适配器
@Component
public class OrderEventWebSocketHandler {
@EventListener
public void handle(OrderPaidEvent event) {
// 实时推送
}
}
五、架构决策与权衡
5.1 什么时候选择六边形架构?
适合的场景:
-
业务逻辑复杂、长期演进的系统
-
需要频繁进行技术栈替换
-
对可测试性要求高
-
领域驱动设计(DDD)项目
不适合的场景:
-
简单的CRUD应用(过度设计)
-
原型验证阶段
-
小团队快速迭代(初期成本较高)
5.2 常见的架构陷阱
过度抽象:为每个接口都创建抽象,导致代码膨胀
// 避免这样
interface OrderRepository {}
interface OrderCache {}
interface OrderQueue {}
interface OrderLogger {}
// 80%的场景只需要Repository
贫血的领域模型:把业务逻辑放在应用层
// 错误:业务逻辑在应用层
public void payOrder(OrderId id) {
Order order = repository.findById(id);
if (order.getStatus() != CREATED) throw…
order.setStatus(PAID); // 直接set
repository.save(order);
}
// 正确:业务逻辑在领域层
public void payOrder(OrderId id) {
Order order = repository.findById(id);
order.pay(); // 领域方法封装逻辑
repository.save(order);
}
事务边界混乱:事务应该在应用层控制
// 正确:事务在应用层
@Transactional
public void payOrder(…) {
order.pay();
repository.save(order);
eventPublisher.publish(order.getEvents());
}
六、与其他架构风格的对比
6.1 传统三层架构 vs 六边形架构
| 依赖方向 | 上层依赖下层 | 所有依赖指向领域 |
| 业务独立性 | 依赖技术框架 | 完全独立 |
| 测试难度 | 需要启动容器 | 单元测试友好 |
| 代码组织 | 按技术分层 | 按业务功能 |
6.2 整洁架构 vs 六边形架构
整洁架构是六边形架构的演进,它们核心思想一致,只是表述不同:
整洁架构 六边形架构
┌─────────────┐ ┌─────────────┐
│ Entities │ │ Domain │
├─────────────┤ ├─────────────┤
│ Use Cases │ │ Application │
├─────────────┤ ├─────────────┤
│ Gateway │ │ Ports │
├─────────────┤ ├─────────────┤
│ Frameworks │ │ Adapters │
└─────────────┘ └─────────────┘
七、实战经验总结
7.1 项目落地要点
渐进式重构:不必一次性重构整个系统,可以逐个模块进行
代码生成:使用代码生成工具减少重复的转换代码
文档维护:保持架构决策记录(ADR)
团队培训:确保团队理解架构理念
7.2 监控与运维
六边形架构为监控提供了清晰的接入点:
@Aspect
@Component
public class PortMonitoringAspect {
@Around("@within(InputPort) || @annotation(InputPort)")
public Object monitorInputPort(ProceedingJoinPoint pjp) {
long start = System.currentTimeMillis();
String portName = pjp.getSignature().toShortString();
try {
Object result = pjp.proceed();
recordMetrics(portName, System.currentTimeMillis() – start, true);
return result;
} catch (Throwable t) {
recordMetrics(portName, System.currentTimeMillis() – start, false);
throw t;
}
}
}
八、未来演进
随着云原生和Serverless的普及,六边形架构的价值更加凸显:
GraalVM原生镜像:纯净的领域层更容易编译为原生镜像
Function as a Service:应用服务可以直接包装为FaaS函数
多运行时架构:不同的适配器可以部署在不同的运行时
结语
六边形架构不是银弹,但它确实为解决微服务架构中的技术耦合问题提供了优雅的方案。通过明确的边界划分和依赖倒置,我们能够构建出:
-
可维护:业务逻辑清晰,变更成本低
-
可测试:单元测试快速,覆盖率容易提升
-
可演进:技术栈替换风险小,架构腐化速度慢
在实际项目中,不必追求理论的完美实现,而是要根据团队情况和业务需求,选择合适的设计程度。架构的本质是管理复杂性,而不是增加复杂性。
核心思想总结:
-
业务逻辑是核心,基础设施是细节
-
依赖方向永远向内
-
端口定义契约,适配器实现细节
-
可测试性是架构健康度的关键指标
希望这篇文章能帮助你在实际项目中更好地应用六边形架构。如果你有任何问题或实践经验想要分享,欢迎在评论区讨论。
