Clean / Hexagonal Architecture(整洁 / 六边形架构)
解决什么问题:让业务逻辑不依赖技术细节——数据库、UI、框架都是可替换的"外部"—— 不分层 = 业务代码里塞满 SQL、HTTP、ORM,业务改不动、技术升级也改不动。 Clean/Hexagonal = 用"依赖倒置"把业务核心与外部细节隔开。
它是什么
Clean Architecture(Robert Martin)和 Hexagonal Architecture(Alistair Cockburn)是同一思想的不同表达:
核心思想:把系统分成同心圆——
- 最内层:企业级业务规则(跨 BC 复用)
- 中间层:应用业务规则(用例编排)
- 外层:接口适配器(Web、DB、消息、CLI)
- 最外层:框架与驱动(Spring、Quarkus、ORM)
依赖规则:外层依赖内层,内层绝不依赖外层。
┌──────────────┐
│ Frameworks │ ← Spring, Hibernate
└──────┬───────┘
↓ 依赖
┌──────┴───────┐
│ Interface │ ← Controllers, Repositories Impl
│ Adapters │
└──────┬───────┘
↓ 依赖
┌──────┴───────┐
│ Application │ ← Use Cases / Services
│ Business │
└──────┬───────┘
↓ 依赖
┌──────┴───────┐
│ Enterprise │ ← Aggregates, Domain Services
│ Business │
└──────────────┘Cockburn 原话(Hexagonal):"Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases."
V2 视角(V2.7 B4 段 4 "抽象边界"):Clean/Hexagonal 是抽象原则在架构层的具体实现——业务核心是抽象,外层是具体实现。
关键属性
| 属性 | 值/含义 |
|---|---|
| 依赖方向 | 外 → 内(单向,永不反向) |
| 核心 | Enterprise Business Rules(聚合、领域服务) |
| 接口 | 内层定义 Port,外层实现 Adapter |
| 替换 | 数据库从 MySQL 换 PG = 只改 Adapter |
| 测试 | 内层可独立测试,不需要 Spring / DB |
| 框架 | 业务核心不 import 任何框架 |
| 反模式 | 业务代码里出现 import javax.persistence.* |
| 判错标准 | 单元测试需要启动 Spring = 架构已坏 |
怎么用
场景 1:分层代码骨架(Java + Spring)
// 1. 领域层(最内层)—— 不依赖任何框架
package com.example.order.domain;
public class Order { ... } // 聚合根
public class PlaceOrderUseCase { // 用例
private final OrderRepository repo; // ← 接口定义在领域层
private final DomainEventPublisher events;
public OrderId execute(PlaceOrderCommand cmd) {
Order order = Order.place(cmd);
repo.save(order);
events.publish(new OrderPlaced(order.id()));
return order.id();
}
}
public interface OrderRepository { // ← Port:领域定义
Order findById(OrderId id);
void save(Order o);
}// 2. 应用服务层(可选——编排用例)
package com.example.order.application;
@Service
public class OrderApplicationService {
private final PlaceOrderUseCase placeOrder;
@Transactional
public OrderId placeOrder(PlaceOrderRequest req) {
return placeOrder.execute(req.toCommand());
}
}// 3. 接口适配层——Web Controller + DB Adapter
package com.example.order.interfaces.web;
@RestController
public class OrderController {
private final OrderApplicationService app;
@PostMapping("/orders")
public ResponseEntity<OrderResponse> place(@RequestBody OrderRequest req) {
OrderId id = app.placeOrder(req);
return ResponseEntity.created(...).body(...);
}
}
// 4. 基础设施 Adapter——实现 Port
package com.example.order.infrastructure.persistence;
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext private EntityManager em;
public Order findById(OrderId id) {
return em.find(Order.class, id.value()); // ← 框架在这里
}
public void save(Order o) { em.persist(o); }
}场景 2:领域层单元测试(不启动 Spring)
class PlaceOrderUseCaseTest {
// Fake Adapter——纯内存实现
static class InMemoryOrderRepo implements OrderRepository {
Map<OrderId, Order> store = new HashMap<>();
public Order findById(OrderId id) { return store.get(id); }
public void save(Order o) { store.put(o.id(), o); }
}
@Test
void places_order_and_publishes_event() {
var repo = new InMemoryOrderRepo();
var events = new RecordingPublisher();
var useCase = new PlaceOrderUseCase(repo, events);
OrderId id = useCase.execute(testCommand());
assertThat(repo.store).containsKey(id);
assertThat(events.published).hasSize(1);
}
}
// 测试运行:0 毫秒,不需要任何 Spring 上下文场景 3:替换技术栈(架构的"可换性"证明)
需求:MySQL → PostgreSQL + JPA → MyBatis
- @Repository
- public class JpaOrderRepository implements OrderRepository { ... }
+ @Repository
+ public class MybatisOrderRepository implements OrderRepository {
+ @Autowired private OrderMapper mapper; // MyBatis mapper
+
+ public Order findById(OrderId id) {
+ OrderPO po = mapper.selectById(id.value());
+ return OrderMapper.toDomain(po);
+ }
+ }领域层 0 改动——Order、PlaceOrderUseCase、OrderRepository 接口全不动。
V2 B6 段 3 "折衷":Clean/Hexagonal 适合核心业务长期演化的系统;如果系统是一次性 CRUD 工具,过度分层反而增加成本。
场景 4:识别"框架入侵"——架构腐化的早期信号
❌ 领域层出现 import org.springframework.*;
❌ 聚合根里有 @Entity, @Table, @Column
❌ UseCase 注入 @Autowired RepositoryImpl(具体类)
❌ 单元测试要 @SpringBootTest 才能跑
✅ 领域层只有 import java.* + 项目内部包
✅ 聚合根是纯 POJO / record
✅ UseCase 注入 Repository(接口)
✅ 单元测试 0 启动,毫秒级运行常见误区
- "分层架构 = Clean Architecture"——错。传统分层(Web → Service → DAO)依赖方向是单向向下——但业务层会 import DAO 接口。Clean 要求业务层只定义接口,实现下沉
- "Clean 太重,小项目不用"——半对。小项目可省"企业级业务规则"层,但依赖规则不可省——业务代码不能 import 框架
- "Clean = DDD"——错。Clean 是架构风格,DDD 是建模方法。两者正交但强相关——Clean 是 DDD 落地的常见载体
- "包结构 = 架构"——错。包名分
controller / service / repository不等于 Clean——那是"按技术分层",不是"按业务聚合"。V2 B3 段 5:先按 BC / 聚合分包,再在 BC 内分层 - "Clean = 100% 框架无关"——过度。关键业务核心框架无关即可,外层(Web、DB)允许用框架——框架是工具,不是污染
关键洞察
Clean/Hexagonal 的本质 = "业务核心是雇主,框架是雇员"。
业务核心定义接口(Port),框架 / DB / Web 提供实现(Adapter)—— 业务核心永远不依赖雇员的实现细节。
V2 B4 抽象 + V2 B3 分解 的组合拳:用抽象让业务核心稳定, 用分解让 BC / 聚合可独立演化。判错标准 = 单元测试能否毫秒级启动。 不能 = 架构已被框架污染。
相关链接
- Views & Decisions——Clean 在视图层如何表达
- Microservice or Monolith——Clean 内部的模块如何映射到部署
- Interface Contract——Port / Adapter 的契约定义
- [V3 涌现作为] · 抽象——Clean 是抽象原则的架构形态
- [V3 涌现作为] · 分解——Clean 是分解后的稳定核心