作者:Killian(重庆后端开发者)|架构落地实践篇|更新日期:2025-06-12 | 字数:5000字
“六边形架构真好,一切皆接口、Mock无压力!”
这种说法我们听得太多,但很多后端工程师——包括我自己早期在用 Clean/Hexagonal 时都会碰到几个问题:
为什么会这样?是架构错了吗?不是。
问题出在:我们没有一个真正面向落地系统的参考项目。
直到我发现了 hexagonal-architecture-java 这个开源项目。
这篇文章将结合我过去在支付系统中踩过的坑,以及这个项目的结构,完整写出一篇超 5000 字的实战架构落地博客,告诉你:
Hexagonal Architecture 在真实工程中如何助力 Mock 测试、实现高可维护性与扩展性,而不是一个玩具结构。
我们先来回顾真实项目中你一定遇到的场景:
这是因为你的代码结构没有隔离行为边界。
在 hexagonal-architecture-java 项目中,他们做得非常明确:
所有业务规则决策 + 外部服务依赖点,都抽象为
Port
接口,由UseCase
使用,外部通过Adapter
实现。
这就是 hexagonal 的核心。
先看一下项目目录结构:
hexagonal-architecture-java/
├── model # 领域模型(不可依赖任何其他层)
├── application # UseCase + Port(业务逻辑核心)
├── adapters/ # 适配器(实现 port,对接外部世界)
│ ├── in/ # 输入端(controller、cli、api)
│ └── out/ # 输出端(db、mq、http、redis)
└── bootstrap # 启动器,装配 Adapter
关键抽象:
CreateCartUseCase
/ AddItemToCartUseCase
CartRepository
/ ProductInfoClient
CartRepositoryJpaAdapter
、ProductInfoHttpClientAdapter
在启动时,我们根据环境(测试、生产)注入不同 Adapter,实现灵活 Mock 与真实连接的切换。
我们以“添加商品到购物车”为例。
public class AddItemToCartUseCaseImpl implements AddItemToCartUseCase {
private final CartRepository cartRepository;
private final ProductInfoClient productClient;
public void addItem(AddItemCommand cmd) {
Cart cart = cartRepository.findById(cmd.getCartId())
.orElseThrow(() -> new IllegalArgumentException("cart not found"));
ProductInfo info = productClient.getInfo(cmd.getProductId());
cart.addItem(cmd.getProductId(), info.getPrice());
cartRepository.save(cart);
}
}
你会发现:
public interface ProductInfoClient {
ProductInfo getInfo(String productId);
}
public interface CartRepository {
Optional<Cart> findById(CartId id);
void save(Cart cart);
}
@Repository
public class CartRepositoryJpaAdapter implements CartRepository {
private final JpaCartRepository jpaRepo;
public void save(Cart cart) {
jpaRepo.save(mapper.toEntity(cart));
}
...
}
在测试环境中,只需注入 InMemory 实现或者 Mock。
我们来看完整测试类:
@ExtendWith(MockitoExtension.class)
class AddItemToCartUseCaseTest {
@Mock CartRepository cartRepository;
@Mock ProductInfoClient productClient;
@InjectMocks AddItemToCartUseCaseImpl useCase;
@Test
void should_add_item_and_save() {
Cart cart = new Cart(new CartId(UUID.randomUUID()));
when(cartRepository.findById(any())).thenReturn(Optional.of(cart));
when(productClient.getInfo(any())).thenReturn(new ProductInfo("p1", BigDecimal.TEN));
useCase.addItem(new AddItemCommand(cart.getId(), "p1", 1));
ArgumentCaptor<Cart> captor = ArgumentCaptor.forClass(Cart.class);
verify(cartRepository).save(captor.capture());
Cart saved = captor.getValue();
assertEquals(1, saved.getItems().size());
assertEquals(BigDecimal.TEN, saved.getItems().get(0).getPrice());
}
}
这个测试的意义是:我们不在意 Redis、MQ、HTTP、数据库连接,只关心:
真实项目中可以轻松验证逻辑边界。
参考 hexagonal-architecture-java,我重构了我们项目中的支付逻辑,提取出:
interface RiskChecker {
boolean pass(Order order);
}
interface PaymentGateway {
String prepay(Order order);
}
interface PaymentRepository {
void save(Order, String payUrl);
}
interface PaymentNotifier {
void notifySuccess(Order);
}
最终生成的 UseCase 如下:
public void pay(PayRequest request) {
if (!riskChecker.pass(request)) throw new RiskRejectException();
String url = gateway.prepay(request);
repository.save(request, url);
notifier.notifySuccess(request);
}
测试环境中可以:
结构特征 | 好处 |
---|---|
UseCase 不依赖 Spring Bean | 测试启动快、mock 容易 |
所有行为都经 Port | 可模拟、可替代、可监控 |
Adapter 单一职责 | 便于 Mock/fake/集成替换 |
Bootstrap 分环境装配 | 测试、灰度、生产灵活切换 |
真正让 Mock 高效的不是“mock 框架”,而是代码结构为 Mock 做好准备。
hexagonal-architecture-java:https://github.com/saw303/hexagonal-architecture-java
推荐同步阅读:
你不是不会写测试,你的架构不让你写。
Hexagonal Architecture 的意义,从来不是“听起来解耦”,而是让测试回归理性、让逻辑清晰独立、让协作高效。
Mock 不应该是框架的战斗,而应该是结构的胜利。
作者:Killian,重庆后端开发者,热衷探索真实系统中的可维护性、可测试性架构设计。欢迎技术Leader和猎头朋友联系交流。
本文由 @killian 原创,转载请注明出处。
☕ 请作者喝杯咖啡,持续更新更深入的干货
彩蛋时间:如果你看到了这里,说明你是那种喜欢动手实战的人。那我悄悄分享一个开发圈流传的工具试用入口,貌似跟高效调试很有关系,地址也挺特别的:
入口
据说注册还能解锁一些隐藏功能,懂的都懂(别外传 )