Skip to content

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) ​

java
// 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);
}
java
// 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());
  }
}
java
// 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) ​

java
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

diff
- @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 启动,毫秒级运行

常见误区 ​

  1. "分层架构 = Clean Architecture"——错。传统分层(Web → Service → DAO)依赖方向是单向向下——但业务层会 import DAO 接口。Clean 要求业务层只定义接口,实现下沉
  2. "Clean 太重,小项目不用"——半对。小项目可省"企业级业务规则"层,但依赖规则不可省——业务代码不能 import 框架
  3. "Clean = DDD"——错。Clean 是架构风格,DDD 是建模方法。两者正交但强相关——Clean 是 DDD 落地的常见载体
  4. "包结构 = 架构"——错。包名分 controller / service / repository 不等于 Clean——那是"按技术分层",不是"按业务聚合"。V2 B3 段 5:先按 BC / 聚合分包,再在 BC 内分层
  5. "Clean = 100% 框架无关"——过度。关键业务核心框架无关即可,外层(Web、DB)允许用框架——框架是工具,不是污染

关键洞察 ​

Clean/Hexagonal 的本质 = "业务核心是雇主,框架是雇员"。

业务核心定义接口(Port),框架 / DB / Web 提供实现(Adapter)—— 业务核心永远不依赖雇员的实现细节。

V2 B4 抽象 + V2 B3 分解 的组合拳:用抽象让业务核心稳定, 用分解让 BC / 聚合可独立演化。判错标准 = 单元测试能否毫秒级启动。 不能 = 架构已被框架污染。

相关链接 ​

从名家方法论与工程化思路中蒸馏出自己的工程体系。