mcpField
C
clean-architecture

clean-architecture

github.com/giuseppe-trisciuoglio开发3664.0k 次安装7 周前更新
代码仓库

关于 clean-architecture

这项技能为 Java 21+、Spring Boot 3.5+ 应用提供整洁架构、六边形架构和 DDD 战术模式指导,强调框架无关的领域逻辑、依赖内向和分层测试。

主要功能

  1. 01四层结构与依赖规则指导
  2. 02框架无关的领域建模
  3. 03端口与适配器设计
  4. 04DDD 实体、值对象和事件
  5. 05分层测试与架构合规检查

适用场景

  1. 01规划新的 Spring Boot 应用结构
  2. 02重构紧耦合代码
  3. 03将领域逻辑与框架分离
  4. 04在无 Spring 上下文时测试业务逻辑

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill clean-architecture

安装后在 Claude Code 中输入/clean-architecture确认已加载。

附带文件references/java-clean-architecture.mdreferences/spring-boot-implementation.md

详细介绍

Spring Boot 的整洁架构、六边形架构与 DDD

概述

本技能全面介绍如何在 Java 21+ Spring Boot 3.5+ 应用中实现整洁架构、六边形架构(端口与适配器)以及领域驱动设计的战术模式。通过恰当的分层和依赖管理,确保职责清晰分离、领域逻辑独立于框架,并打造高度可测试的代码库。

适用场景

  • 为职责清晰分离的新 Spring Boot 应用设计架构
  • 将紧密耦合的代码重构为可测试的分层架构
  • 实现独立于框架和基础设施的领域逻辑
  • 设计可替换实现的端口和适配器
  • 应用领域驱动设计的战术模式(实体、值对象、聚合)
  • 创建不依赖 Spring 上下文的可测试业务逻辑

说明

1. 理解核心概念
整洁架构分层(依赖规则)

依赖向内流动。内层不应了解外层。

层职责Spring Boot 对应项
领域实体、值对象、领域事件、仓储接口domain/ - 无 Spring 注解
应用用例、应用服务、DTO、端口application/ - @Service、@Transactional
基础设施框架、数据库、外部 APIinfrastructure/ - @Repository、@Entity
适配器控制器、表示器、外部网关adapter/ - @RestController
六边形架构(端口与适配器)
  • 领域核心:纯 Java 业务逻辑,不依赖框架
  • 端口:定义契约的接口(驱动端和被驱动端)
  • 适配器:具体实现(JPA、REST、消息传递)
领域驱动设计战术模式
  • 实体:具有身份标识和生命周期的对象(例如 Order、Customer)
  • 值对象:不可变,由属性定义(例如 Money、Email)
  • 聚合:以根实体为界限的一致性边界
  • 领域事件:记录重要的业务事件
  • 仓储:持久化抽象,在基础设施层中实现
2. 组织包结构

遵循以下基于功能的包组织方式:

com.example.order/
├── domain/
│   ├── model/              # Entities, value objects
│   ├── event/              # Domain events
│   ├── repository/         # Repository interfaces (ports)
│   └── exception/          # Domain exceptions
├── application/
│   ├── port/in/            # Driving ports (use case interfaces)
│   ├── port/out/           # Driven ports (external service interfaces)
│   ├── service/            # Application services
│   └── dto/                # Request/response DTOs
├── infrastructure/
│   ├── persistence/        # JPA entities, repository adapters
│   └── external/           # External service adapters
└── adapter/
    └── rest/               # REST controllers
3. 实现领域层(不依赖框架)

领域层不得依赖 Spring 或任何框架。

  • 使用 Java record 创建带内置验证的不可变值对象
  • 将业务逻辑放在实体中,而非服务中(充血领域模型)
  • 在领域层定义仓储接口(端口)
  • 使用强类型 ID,避免 ID 混淆
  • 实现领域事件,以解耦副作用
  • 使用工厂方法创建实体,以确保不变量
4. 实现应用层
  • 在 application/port/in/ 中创建用例接口(驱动端口)
  • 在 application/port/out/ 中创建外部服务接口(被驱动端口)
  • 使用 @Service 和 @Transactional 实现应用服务
  • 使用 DTO 处理请求和响应,并与领域模型分开
  • 操作成功后发布领域事件
5. 实现基础设施层(适配器)
  • 在 infrastructure/persistence/ 中创建 JPA 实体
  • 实现仓储适配器,在领域实体与 JPA 实体之间进行映射
  • 使用 MapStruct 或手动映射器进行领域对象与 JPA 对象之间的转换
  • 配置条件 Bean,以支持替换实现
  • 将基础设施相关事项与领域逻辑隔离
6. 实现适配器层(REST)
  • 在 adapter/rest/ 中创建 REST 控制器
  • 注入用例接口,而非实现类
  • 对 DTO 使用 Bean Validation
  • 返回恰当的 HTTP 状态码和响应
  • 使用全局异常处理器处理异常
7. 应用最佳实践
  1. 依赖规则:领域层不依赖 Spring 或其他框架
  2. 不可变值对象:使用 Java record 创建带内置校验的值对象
  3. 富领域模型:将业务逻辑放在实体中,而不是服务中
  4. 仓储模式:由领域层定义接口,由基础设施层实现
  5. 领域事件:将副作用与主要操作解耦
  6. 构造器注入:通过 final 字段注入必需依赖
  7. DTO 映射:将领域模型与 API 契约分开
  8. 事务边界:在应用服务中放置 @Transactional
  9. 工厂方法:使用 Entity.create() 在构造时确保不变量
  10. 分离 JPA 实体:通过映射器将领域实体与 JPA 实体分开
8. 验证架构合规性

实现每一层后,验证依赖规则是否得到遵守:

  • 领域层检查:运行 grep -r "@Service\|@Component\|@Autowired" domain/,确保没有 Spring 导入
  • ArchUnit 测试:添加依赖测试,验证领域层没有导入基础设施:
    noClasses().that().resideInPackage("..domain..")
        .should().accessClassesThat().resideInAnyPackage("..spring..", "..infrastructure..");
    
  • 实体暴露检查:验证领域服务绝不返回 JPA 实体
  • 事务检查:确认 @Transactional 只出现在应用层服务中,绝不出现在领域层
9. 编写测试
  • 领域测试:不加载 Spring 上下文的纯单元测试,执行速度快
  • 应用测试:使用 Mockito 模拟端口的单元测试
  • 基础设施测试:使用 @DataJpaTest 和 Testcontainers 进行集成测试
  • 适配器测试:使用 @WebMvcTest 编写控制器测试

示例

示例 1:领域层 - 带领域事件的实体
// domain/model/Order.java
public class Order {
    private final OrderId id;
    private final List<OrderItem> items;
    private Money total;
    private OrderStatus status;
    private final List<DomainEvent> domainEvents = new ArrayList<>();

    private Order(OrderId id, List<OrderItem> items) {
        this.id = id;
        this.items = new ArrayList<>(items);
        this.status = OrderStatus.PENDING;
        calculateTotal();
    }

    public static Order create(List<OrderItem> items) {
        validateItems(items);
        Order order = new Order(OrderId.generate(), items);
        order.domainEvents.add(new OrderCreatedEvent(order.id, order.total));
        return order;
    }

    public void confirm() {
        if (status != OrderStatus.PENDING) {
            throw new DomainException("Only pending orders can be confirmed");
        }
        this.status = OrderStatus.CONFIRMED;
    }

    public List<DomainEvent> getDomainEvents() {
        return List.copyOf(domainEvents);
    }

    public void clearDomainEvents() {
        domainEvents.clear();
    }
}
示例 2:领域层 - 带校验的值对象
// domain/model/Money.java (Value Object)
public record Money(BigDecimal amount, Currency currency) {
    public Money {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new DomainException("Amount cannot be negative");
        }
    }

    public static Money zero() {
        return new Money(BigDecimal.ZERO, Currency.getInstance("EUR"));
    }

    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new DomainException("Currency mismatch");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
}
示例 3:领域层 - 仓储端口
// domain/repository/OrderRepository.java (Port)
public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(OrderId id);
}
示例 4:应用层 - 用例和服务
// application/port/in/CreateOrderUseCase.java
public interface CreateOrderUseCase {
    OrderResponse createOrder(CreateOrderRequest request);
}

// application/dto/CreateOrderRequest.java
public record CreateOrderRequest(
    @NotNull UUID customerId,
    @NotEmpty List<OrderItemRequest> items
) {}

// application/service/OrderService.java
@Service
@RequiredArgsConstructor
@Transactional
public class OrderService implements CreateOrderUseCase {
    private final OrderRepository orderRepository;
    private final PaymentGateway paymentGateway;
    private final DomainEventPublisher eventPublisher;

    @Override
    public OrderResponse createOrder(CreateOrderRequest request) {
        List<OrderItem> items = mapItems(request.items());
        Order order = Order.create(items);

        PaymentResult payment = paymentGateway.charge(order.getTotal());
        if (!payment.successful()) {
            throw new PaymentFailedException("Payment failed");
        }

        order.confirm();
        Order saved = orderRepository.save(order);
        publishEvents(order);

        return OrderMapper.toResponse(saved);
    }

    private void publishEvents(Order order) {
        order.getDomainEvents().forEach(eventPublisher::publish);
        order.clearDomainEvents();
    }
}
示例 5:基础设施层 - JPA 实体和适配器
// infrastructure/persistence/OrderJpaEntity.java
@Entity
@Table(name = "orders")
public class OrderJpaEntity {
    @Id
    private UUID id;
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    private BigDecimal totalAmount;
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItemJpaEntity> items;
}

// infrastructure/persistence/OrderRepositoryAdapter.java
@Component
@RequiredArgsConstructor
public class OrderRepositoryAdapter implements OrderRepository {
    private final OrderJpaRepository jpaRepository;
    private final OrderJpaMapper mapper;

    @Override
    public Order save(Order order) {
        OrderJpaEntity entity = mapper.toEntity(order);
        return mapper.toDomain(jpaRepository.save(entity));
    }

    @Override
    public Optional<Order> findById(OrderId id) {
        return jpaRepository.findById(id.value()).map(mapper::toDomain);
    }
}
示例 6:适配器层 - REST 控制器
// adapter/rest/OrderController.java
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
    private final CreateOrderUseCase createOrderUseCase;

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(
            @Valid @RequestBody CreateOrderRequest request) {
        OrderResponse response = createOrderUseCase.createOrder(request);
        URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(response.id())
            .toUri();
        return ResponseEntity.created(location).body(response);
    }
}
示例 7:领域测试(不加载 Spring 上下文)
class OrderTest {
    @Test
    void shouldCreateOrderWithValidItems() {
        List<OrderItem> items = List.of(
            new OrderItem(new ProductId(UUID.randomUUID()), 2, new Money("10.00", EUR))
        );

        Order order = Order.create(items);

        assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING);
        assertThat(order.getDomainEvents()).hasSize(1);
    }
}
示例 8:应用测试(使用模拟对象的单元测试)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock OrderRepository orderRepository;
    @Mock PaymentGateway paymentGateway;
    @Mock DomainEventPublisher eventPublisher;

    @InjectMocks OrderService orderService;

    @Test
    void shouldCreateAndConfirmOrder() {
        when(paymentGateway.charge(any())).thenReturn(new PaymentResult(true, "tx-123"));
        when(orderRepository.save(any())).thenAnswer(i -> i.getArgument(0));

        OrderResponse response = orderService.createOrder(createRequest());

        assertThat(response.status()).isEqualTo(OrderStatus.CONFIRMED);
        verify(eventPublisher).publish(any(OrderCreatedEvent.class));
    }
}

最佳实践

  • 领域纯粹性:让领域层不包含 Spring 注解和框架导入——不依赖外层
  • 按功能组织包:按业务能力(order/、customer/)而非技术角色组织,每个功能都包含全部四层
  • 不可变值对象:使用 Java record 创建值对象,并在紧凑构造器中内置校验——从设计上保证不可变
  • 富领域模型:将业务逻辑放在实体和聚合中,而不是应用服务中——服务负责编排,实体负责封装
  • 始终进行映射:使用 MapStruct 或手动映射器将 JPA 实体与领域模型分开;绝不在基础设施层之外暴露 JPA 实体
  • 使用领域事件实现解耦:使用 DomainEventPublisher 解耦跨聚合副作用,而不是直接调用服务
  • 在应用层划定事务边界:只在应用服务上放置 @Transactional,绝不放在领域类上
  • 使用工厂方法确保不变量:使用 Entity.create(...) 静态方法,在构造时确保不变量
  • 使用 ArchUnit 强制执行:在测试套件中添加 ArchUnit 测试,验证 Spring 或基础设施导入不会进入领域层
  • 强类型 ID:使用 record OrderId(UUID value),而不是原始 UUID,以避免聚合间 ID 混淆

约束和警告

关键约束
  • 领域层纯粹性:绝不在领域类中添加 Spring 注解(@Entity、@Autowired、@Component)
  • 依赖方向:依赖只能向内指向(领域层 <- 应用层 <- 基础设施层/适配器层)
  • 框架隔离:所有特定于框架的代码都必须留在基础设施层和适配器层
应避免的常见陷阱
  • 贫血领域模型:实体只有 getter/setter,逻辑却放在服务中——应将业务逻辑放在实体中
  • 框架泄漏:在领域层使用 @Entity、@Autowired——保持领域层不依赖框架
  • 延迟加载问题:通过领域模型暴露 JPA 实体——使用映射器进行转换
  • 循环依赖:领域聚合之间存在循环依赖——使用 ID 而非直接引用
  • 缺少领域事件:跨聚合通信时直接调用服务,而不是使用事件
  • 仓储位置不当:在基础设施层定义仓储接口——仓储接口应属于领域层
  • 绕过 DTO:在 API 中直接暴露领域实体——对外部契约始终使用 DTO
性能注意事项
  • 将 JPA 实体与领域模型分开,以避免延迟加载问题
  • 查询操作使用只读事务
  • 对于复杂的读写场景,可考虑 CQRS

参考资料

  • references/java-clean-architecture.md - Java 专属模式(records、sealed classes、强类型 ID)
  • references/spring-boot-implementation.md - Spring Boot 集成(DI 模式、JPA 映射、事务管理)

评论

支持 Markdown