AI辅助DDD微服务开发:从混乱到规范的实践之路
·
引言:AI生成代码的不稳定性困境
在当今软件开发领域,AI辅助编程已成为提高效率的重要手段。然而,在实际项目中,我们发现AI生成的代码存在明显的不稳定性问题:
- 领域模型污染:AI常常在领域层代码中混入审计字段(
createdAt、updatedAt)、技术ID等基础设施关注点 - 架构层次混乱:不同层的职责边界模糊,导致代码难以维护
- 命名不一致:同类组件命名风格各异,增加理解成本
- 测试覆盖不足:生成的代码往往缺乏必要的单元测试
这些问题的根源在于:AI缺乏项目特定的上下文和约束规则。本文将分享我们如何通过制定严格的project_rules.md规范,结合DDD(领域驱动设计)最佳实践,构建一个稳定、可维护的微服务架构。
一、项目规则:AI协作的基石
1.1 规则文件的核心价值
我们创建了.trae/rules/project_rules.md文件,作为AI生成代码的约束基准:
## 通用约束
1. 严格遵循**领域层→基础设施层→应用层→接口层**依赖方向,禁止反向依赖;
2. 统一包结构、命名规范,代码无冗余、无语法错误,关键逻辑加注释;
3. 强制单元测试,禁止无测试代码上线。
## 领域层(核心)
1. 核心职责:定义业务规则、领域模型,实现纯业务逻辑,**禁止包含数据库ID、审计字段等技术代码**;
2. 模型规范:实体、值对象(VO)、聚合、聚合根(Root)、域服务、仓储接口、领域事件;
3. 类名规范:聚合根`Root`结尾、值对象`VO`结尾、枚举`Enum`结尾;
...
1.2 规则的执行机制
- 上下文注入:每次与AI交互时,自动加载规则文件作为系统提示
- 代码审查:通过CI/CD流水线检查代码是否符合规范
- 单元测试:强制要求测试覆盖率≥90%
二、领域层设计:保持纯粹性
2.1 聚合根的正确姿势
问题场景:AI生成的聚合根常常包含技术字段
// ❌ 错误示例:领域层包含技术字段
public class Sku {
private Long id; // 技术ID
private LocalDateTime createdAt; // 审计字段
private LocalDateTime updatedAt; // 审计字段
private String skuCode;
private String name;
// ...
}
规范方案:领域层只关注业务属性
// ✅ 正确示例:领域层保持纯粹
public class Sku extends BaseAggregateRoot<Sku> {
private SkuCode skuCode; // 业务标识
private String name;
private SkuStatus status; // 业务状态
private String category;
private String unit;
private SafetyStockQuantity safetyStock; // 值对象
private ABCClassEnum abcClass; // 枚举
// ... 纯业务属性,无技术字段
protected Sku() {} // JPA要求
private Sku(SkuCode skuCode, String name, String category, String unit) {
this.skuCode = skuCode;
this.name = name.trim();
this.status = SkuStatus.DRAFT;
registerEvent(new SkuCreatedEvent(skuCode.getValue(), name));
}
}
2.2 领域事件的简化发布
问题场景:手动管理领域事件容易遗漏
// ❌ 错误示例:手动发布事件
public void activate() {
this.status = WarehouseStatus.ACTIVE;
eventBus.publish(new WarehouseActivatedEvent(this.warehouseCode));
}
规范方案:继承AbstractAggregateRoot自动管理
// ✅ 正确示例:使用JPA的AbstractAggregateRoot
public class Warehouse extends AbstractAggregateRoot<Warehouse> {
public void activate() {
if (this.status == WarehouseStatus.ACTIVE) {
return; // 幂等性保证
}
this.status = WarehouseStatus.ACTIVE;
registerEvent(new WarehouseActivatedEvent(this.warehouseCode));
}
}
2.3 值对象的设计陷阱
陷阱1:静态常量初始化循环依赖
// ❌ 错误示例:构造器包含校验逻辑
public class Weight {
public static final Weight MIN_WEIGHT = new Weight(BigDecimal.ZERO, MeasurementUnit.KG);
private final BigDecimal value;
private final MeasurementUnit unit;
public Weight(BigDecimal value, MeasurementUnit unit) {
if (value.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("重量不能为负");
}
this.value = value;
this.unit = unit;
}
}
// 问题:MIN_WEIGHT初始化时需要调用构造器,但构造器依赖MIN_WEIGHT.value
// ✅ 正确示例:构造器不包含复杂校验
public class Weight {
private final BigDecimal value;
private final MeasurementUnit unit;
public Weight(BigDecimal value, MeasurementUnit unit) {
this.value = value;
this.unit = unit;
}
public static Weight ofKg(BigDecimal value) {
if (value.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("重量不能为负");
}
return new Weight(value, MeasurementUnit.KG);
}
}
陷阱2:地址值对象的显示问题
// ✅ 正确处理直辖市地址
public class AddressVO {
public String getDisplayAddress() {
StringBuilder sb = new StringBuilder();
sb.append(province);
if (city != null && !city.equals(province)) {
sb.append(city); // 北京市不重复显示"北京市北京市"
}
sb.append(district);
return sb.toString();
}
}
三、基础设施层:技术实现的边界
3.1 PO类的审计字段自动化
问题场景:每次保存都需要手动设置审计字段
// ❌ 错误示例:手动设置审计字段
public Sku save(Sku sku) {
SkuPO po = converter.toPO(sku);
po.setCreatedAt(LocalDateTime.now()); // 容易遗漏
po.setUpdatedAt(LocalDateTime.now());
return jpaRepository.save(po);
}
规范方案:使用JPA审计注解
// ✅ 正确示例:PO类使用审计注解
@Entity
@EntityListeners(AuditingEntityListener.class)
public class SkuPO {
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
3.2 MapStruct转换器的最佳实践
问题场景:DO需要setter方法,破坏封装性
// ❌ 错误示例:暴露所有setter方法
@Data // Lombok生成所有setter
public class Sku {
private SkuCode skuCode;
// ...
}
规范方案:使用全参构造器 + MapStruct配置
// ✅ 正确示例:领域模型使用全参构造器
public class Sku {
private final SkuCode skuCode;
private String name;
private Sku(SkuCode skuCode, String name) {
this.skuCode = skuCode;
this.name = name;
}
// 仅提供必要的业务方法
public void updateName(String name) {
this.name = name;
}
}
// MapStruct配置
@Mapper(componentModel = "spring")
public interface SkuConverter {
@Mapping(target = "skuCode", ignore = true)
@Mapping(target = "status", ignore = true)
Sku toDO(SkuPO po);
// 使用@ObjectFactory处理复杂转换
@ObjectFactory
default Sku createSku(SkuPO po) {
return Sku.fromPO(
SkuCode.of(po.getSkuCode()),
po.getName(),
po.getStatus() != null ? SkuStatus.fromCode(po.getStatus()) : null
// ... 其他参数
);
}
}
3.3 值对象的扁平化映射
// PO类:扁平化存储值对象
@Entity
public class SkuPO {
@Column(name = "safety_stock")
private BigDecimal safetyStock;
@Column(name = "safety_stock_unit")
private String safetyStockUnit;
@Column(name = "length")
private BigDecimal length;
@Column(name = "width")
private BigDecimal width;
}
// DO类:使用值对象封装
public class Sku {
private SafetyStockQuantity safetyStock; // 值对象
private Dimensions dimensions; // 值对象
}
四、应用层:CQRS读写分离
4.1 为什么需要CQRS?
问题场景:每次查询都加载整个聚合
// ❌ 错误示例:查询加载整个聚合
public Customer getCustomer(Long id) {
Customer customer = customerRepository.findById(id);
// 加载了所有关联实体,性能低下
return customer;
}
规范方案:读写分离架构
// ✅ 命令模型:使用领域模型
@Service
public class CustomerCommandService {
@Transactional
public Customer createCustomer(CreateCustomerCommand cmd) {
Customer customer = Customer.create(
CustomerCode.of(cmd.getCustomerCode()),
cmd.getName(),
CustomerType.fromCode(cmd.getType())
);
return customerRepository.save(customer);
}
}
// ✅ 查询模型:使用DTO + 读仓储
@Service
public class CustomerQueryService {
private final CustomerReadRepository readRepository;
@Transactional(readOnly = true)
public PageResponse<CustomerDTO> query(CustomerQuery query) {
return readRepository.queryCustomers(
query.getStatus(),
query.getType(),
query.getName(),
PageRequest.of(query.getPageNum() - 1, query.getPageSize())
);
}
}
4.2 读仓储的设计
// 读仓储接口:支持分页、动态查询
public interface CustomerReadRepository {
Optional<CustomerPO> findById(Long id);
Optional<CustomerPO> findByCustomerCode(String customerCode);
Page<CustomerPO> queryCustomers(String status, String type, String name, Pageable pageable);
List<CustomerPO> findByStatus(String status);
boolean existsByCustomerCode(String customerCode);
}
五、事件溯源与幂等性保证
5.1 领域事件的幂等设计
// ✅ 业务方法内置幂等性检查
public void activate() {
if (this.status == WarehouseStatus.ACTIVE) {
// 幂等:已经是激活状态,不产生新事件
return;
}
this.status = WarehouseStatus.ACTIVE;
registerEvent(new WarehouseActivatedEvent(this.warehouseCode));
}
5.2 Redis分布式幂等检查
@Component
public class RedisIdempotencyChecker {
@Autowired
private RedisTemplate<String, String> redis;
public boolean isProcessed(String eventId, String consumer) {
String key = "event:processed:" + eventId + ":" + consumer;
Boolean success = redis.opsForValue()
.setIfAbsent(key, "1", Duration.ofDays(7));
return !Boolean.TRUE.equals(success);
}
}
5.3 事件消费的标准流程
@Transactional
public void handleWarehouseActivatedEvent(WarehouseActivatedEvent event) {
// 1. 幂等性检查
if (idempotencyChecker.isProcessed(event.getEventId(), "inventory-service")) {
return;
}
// 2. 版本号验证(防止乱序)
WarehousePO warehouse = warehouseReadRepository.findByCode(event.getWarehouseCode());
if (warehouse.getVersion() > event.getVersion()) {
return;
}
// 3. 状态机验证(业务幂等)
if (warehouse.getStatus() == WarehouseStatus.ACTIVE.getCode()) {
return;
}
// 4. 业务处理
inventoryService.initializeWarehouseInventory(event.getWarehouseCode());
// 5. 记录事件处理
eventStore.save(event);
}
六、单元测试的参数化实践
6.1 使用CSV文件管理测试数据
// ✅ 参数化测试示例
@ParameterizedTest
@CsvFileSource(resources = "/testdata/sku-creation-data.csv", numLinesToSkip = 1)
void testCreateSkuWithVariousInputs(String skuCode, String name, String category,
String unit, boolean shouldSucceed) {
if (shouldSucceed) {
assertDoesNotThrow(() -> Sku.create(
SkuCode.of(skuCode), name, category, unit
));
} else {
assertThrows(IllegalArgumentException.class, () -> Sku.create(
SkuCode.of(skuCode), name, category, unit
));
}
}
6.2 测试数据复用
// ✅ 测试数据工厂
public class SkuTestDataFactory {
public static Sku createDefaultSku() {
return Sku.create(
SkuCode.of("SKU001"),
"测试商品",
"电子产品",
"台"
);
}
public static Sku createSkuWithCode(String code) {
Sku sku = createDefaultSku();
// 使用反射或测试专用方法设置skuCode
return sku;
}
}
七、基础设施层的抽象设计
7.1 消息队列接口抽象
// ✅ 抽象MQ接口,支持多种实现
public interface MessageQueuePublisher {
void publish(String topic, Object message);
void publish(String topic, String key, Object message);
}
// Kafka实现
@Component
@ConditionalOnProperty(name = "mq.type", havingValue = "kafka")
public class KafkaPublisher implements MessageQueuePublisher {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Override
public void publish(String topic, Object message) {
kafkaTemplate.send(topic, toJson(message));
}
}
// RabbitMQ实现
@Component
@ConditionalOnProperty(name = "mq.type", havingValue = "rabbitmq")
public class RabbitMQPublisher implements MessageQueuePublisher {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void publish(String topic, Object message) {
rabbitTemplate.convertAndSend(topic, toJson(message));
}
}
八、总结与最佳实践
8.1 核心原则
| 层次 | 核心职责 | 禁止事项 |
|---|---|---|
| 领域层 | 纯业务逻辑 | 技术ID、审计字段、数据库操作 |
| 基础设施层 | 技术实现 | 业务逻辑、直接暴露领域模型 |
| 应用层 | 用例编排 | 直接操作PO、跨聚合直接调用 |
| 接口层 | 外部接入 | 业务逻辑、数据转换逻辑 |
8.2 AI协作的关键点
- 规则先行:在与AI交互前,明确项目规范
- 上下文管理:保持规则文件的持续更新
- 代码审查:AI生成的代码必须经过人工审查
- 测试驱动:先写测试,再让AI生成实现
8.3 常见陷阱清单
- 领域层包含技术字段(id、createdAt、updatedAt)
- 值对象嵌套超过3层
- 聚合根缺少领域事件发布
- MapStruct转换破坏领域封装
- 查询操作加载整个聚合
- 领域事件缺少幂等性保证
- 单元测试数据硬编码
- 枚举类缺少displayName
结语
通过建立严格的project_rules.md规范,我们成功解决了AI生成代码的不稳定性问题。这套规范不仅约束了AI的行为,也为团队成员提供了统一的设计准则。
在实际项目中,我们发现:
- 代码质量提升:遵循规范的代码更易维护
- 开发效率提高:减少返工和重构
- 团队协作改善:统一的规范降低沟通成本
- AI协作顺畅:规则文件让AI理解项目上下文
DDD不是银弹,但在复杂业务场景下,它提供了清晰的架构边界。结合AI辅助编程,我们可以更快地交付高质量的软件系统。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)