概述
电商系统的 DDD 实践已经持续了一年,订单、库存、支付、物流四个限界上下文在模块化单体中稳定运行,ArchUnit 守护着包边界,领域事件驱动着跨上下文协作,陈述式代码表达着业务意图。但季度架构评审中,架构师打开 SonarQube 和 ArchUnit 报告,皱起了眉头:订单模块的 Order 实体又退化成了贫血模型——setStatus 散落在 3 个 Service 中;OrderRepository 的查询方法已经膨胀到 25 个;支付模块的领域层直接依赖了外部支付网关的 ExternalPaymentResponse DTO;新来的同事在通知模块的领域层新建了一个叫 NotificationInfo 的实体;Inventory 聚合因为包含太多实体,乐观锁冲突率已经飙到 15%。这些不是新问题,而是 DDD 实践中反复出现的典型反模式——它们在团队扩张、业务压力、新人融入时悄然滋生,一点点侵蚀着领域模型的清晰性。
本文正是为解决这些问题而生。它将本系列前十四篇的全部 DDD 知识——战略设计、战术 DDD、模块化单体、事件驱动、领域事件、防腐层 ACL、聚合设计、陈述式代码——转化为 12+ DDD 反模式的精确定义与排查手册,建立领域模型腐化、架构边界腐化、代码表达腐化三大排查决策树,组合 ArchUnit、SonarQube、Spring Cloud Contract 等自动化工具链,并探索 AI 辅助代码审查在反模式检测中的应用。以电商团队季度架构评审发现的 12 项反模式为贯穿案例,从评审会议识别、六步排查法逐项修复,到工具链自动化防线建立,完整展示 DDD 腐化的治理闭环。读完本文,你将在下一次代码评审或架构审计中,不再仅凭直觉说“代码写得不好”,而是精确指出“这是一个贫血模型反模式,违反本系列第 2 篇的聚合根不变量保护原则,修复方案是将 setStatus 封装为 confirmPayment() 行为方法,并通过这条 ArchUnit 规则防止复发”。
核心要点:
- 六步排查法:现象描述→指标定位→工具诊断→根因确认→方案修正→验证恢复,每步有明确的命令和输出。
- 12+ 反模式手册:贫血模型、聚合过大、资源库方法膨胀、事件粒度不当、缺少防腐层、跨界上下文直调、统一语言缺失、值对象误用、事件未幂等、CQRS 过度设计、未遵循小聚合、陈述式代码退化。
- 三大决策树:领域模型腐化、架构边界腐化、代码表达腐化,由表及里直达根因与修正路径。
- 诊断命令链:ArchUnit + SonarQube + Spring Cloud Contract + jQAssistant + AI 辅助审查。
- 连锁推演:揭示反模式间隐秘关联——贫血模型→数据不一致,聚合过大→LazyInitializationException,缺少 ACL→领域模型污染。
- 电商贯穿案例:从季度评审 12 项反模式识别、分级、修复到防复发,全闭环实录。
文章组织架构图
flowchart TB
A["1. DDD 反模式排查方法论:六步排查法"] --> B["2. 反模式剖析(上):领域模型腐化"]
A --> C["3. 反模式剖析(中):架构边界腐化"]
A --> D["4. 反模式剖析(下):代码表达腐化"]
B --> E["5. 三大腐化域的排查决策树"]
C --> E
D --> E
E --> F["6. 生产诊断工具链实战"]
F --> G["7. AI 辅助代码审查的探索实践"]
G --> H["8. 反模式关联推演:连锁反应链"]
H --> I["9. 贯穿案例:电商季度架构评审 12 项反模式排查实录"]
I --> J["10. 与前后系列的衔接"]
J --> K["11. 面试高频专题"]
L["附录:速查表与配置清单"] --> A
L --> B
L --> C
L --> D
L --> F
L --> G
classDef default fill:#f1f5f9,stroke:#334155,stroke-width:1.5px,color:#1e293b
classDef appendix fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#1e3a8a
class L appendix
架构图说明:
- 总览说明:全文 11 个模块,以六步排查法为方法论基石,系统归类 12+ 反模式,构建三大决策树,通过诊断工具链和连锁推演深化认知,探索 AI 辅助审查,最后以贯穿案例和面试专题收尾,附录提供速查工具。
- 逐模块说明:模块 1 提供可复用的标准化排查流程;模块 2-4 将所有 DDD 知识转化为反模式诊断手册,逐一剖析;模块 5 以决策树形式提供排查“导航地图”;模块 6 展示自动化防线;模块 7 引入 AI 作为辅助防线;模块 8 揭示反模式间的蝴蝶效应;模块 9 用真实评审串联全部知识;模块 10-11 缝合全系列并巩固面试能力;附录作为速查参考。
- 设计原理映射:文章结构遵循“方法论→分类诊断→决策系统→工具落地→智能增强→关联分析→实战验证”的认知路径,确保从理论到实践的完整覆盖。
- 工程联系与关键结论:每一个 DDD 反模式都不是偶然的,它背后一定对应着一个被违背的设计原则。六步排查法和三大决策树的价值,不是替代经验,而是将 DDD 原则的守护从“人工记忆”变为“自动化检测”。掌握这套方法论,你能在下一次代码评审或架构审计中,精确指出每一个反模式的名称、违反的原则、对应的修正方案和防止复发的 ArchUnit 规则。
1. DDD 反模式排查方法论:六步排查法
在开始剖析各类反模式之前,需要建立一套结构化的排查方法论。这套方法论被称为 六步排查法,它将反模式的识别、诊断、修复和验证固化为六个标准步骤,每一步都有明确的输入、输出和适用工具。
1.1 六步排查法流程图
flowchart LR
S1["步骤1:现象描述"] --> S2["步骤2:指标定位"]
S2 --> S3["步骤3:工具诊断"]
S3 --> S4["步骤4:根因确认"]
S4 --> S5["步骤5:方案修正"]
S5 --> S6["步骤6:验证恢复"]
subgraph Input1["输入"]
I1["团队反馈、Bug现象、代码走查印象"]
end
subgraph Output1["输出"]
O1["初步判定反模式类型"]
end
S1 -.- I1
S1 -.- O1
subgraph Input2["输入"]
I2["ArchUnit扫描结果、SQL日志、SonarQube指标、API响应时间"]
end
subgraph Output2["输出"]
O2["定量证据:方法占比、JOIN数、方法数量等"]
end
S2 -.- I2
S2 -.- O2
subgraph Input3["输入"]
I3["针对性ArchUnit规则、jQAssistant查询、grep统计、jstack"]
end
subgraph Output3["输出"]
O3["违规类/方法/包列表,具体数值"]
end
S3 -.- I3
S3 -.- O3
subgraph Input4["输入"]
I4["领域设计原则(对应系列篇章)"]
end
subgraph Output4["输出"]
O4["违反的具体原则与设计根因"]
end
S4 -.- I4
S4 -.- O4
subgraph Input5["输入"]
I5["重构PR、代码对比、架构委员会评审"]
end
subgraph Output5["输出"]
O5["修正代码 + 防复发ArchUnit规则"]
end
S5 -.- I5
S5 -.- O5
subgraph Input6["输入"]
I6["回归测试套件、SonarQube质量门禁、性能测试报告"]
end
subgraph Output6["输出"]
O6["反模式关闭确认,架构看板更新"]
end
S6 -.- I6
S6 -.- O6
classDef subgraphStyle fill:#f8fafc,stroke:#94a3b8,stroke-width:1.5px
classDef stepStyle fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#1e3a8a
classDef inputStyle fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#92400e
classDef outputStyle fill:#ede9fe,stroke:#8b5cf6,stroke-width:1.5px,color:#4c1d95
class Input1,Output1,Input2,Output2,Input3,Output3,Input4,Output4,Input5,Output5,Input6,Output6 subgraphStyle
class S1,S2,S3,S4,S5,S6 stepStyle
class I1,I2,I3,I4,I5,I6 inputStyle
class O1,O2,O3,O4,O5,O6 outputStyle
图表说明:
- 主旨概括:六步排查法将反模式治理从“感觉”转化为“数据驱动”的闭环流程,每一步都有明确的输入输出,形成可追溯的治理记录。
- 逐层/逐元素分解:步骤1将模糊问题表述为精确反模式名称;步骤2用度量指标定量化问题;步骤3通过自动化工具(ArchUnit、jQAssistant、grep等)获取精确的违规点;步骤4对照系列前十四篇的原则进行根因定位;步骤5产出具体代码修正方案和预防规则;步骤6通过自动化测试和门禁确认修复有效。
- 设计原理映射:该流程融合了《实现领域驱动设计》中“模型健康检查”与持续交付中“质量门禁”的思想,将架构治理工程化。
- 工程联系与关键结论:六步排查法是连接 DDD 原则与工程实践的桥梁。在实际季度评审中,架构师用这套方法对每个反模式进行编号跟踪,确保不遗漏、不误判。
1.2 六步排查法详细操作指南
以下以电商系统中典型的“订单模块贫血模型”为例,展示每一步的具体操作。
步骤1:现象描述
- 输入:代码评审反馈“订单模块代码难以理解,修改状态到处都写
order.setStatus(xxx)”,以及 bug 统计“状态校验逻辑不一致导致 3 次线上故障”。 - 操作:精确描述为“限界上下文:订单上下文;实体:
Order;现象:Order实体只有 getter/setter,所有业务逻辑集中在OrderService中,该 Service 类超过 800 行,setStatus方法在 3 个不同 Service 中被调用。” - 输出:初步判定为“贫血模型反模式”。
步骤2:指标定位
- 输入:从 SonarQube 获取
Order类的复杂度指标,从代码统计获取方法数量。 - 操作:
- 运行 ArchUnit 基础扫描,得到
Order实体公有方法中 getter/setter 占比 95%。 - SonarQube 显示
OrderService的圈复杂度为 87,技术债务比为 5.2%。 - 使用
grep -c "setStatus" **/*.java统计到 12 处调用。
- 运行 ArchUnit 基础扫描,得到
- 输出:定量证据——贫血比例 95%,12 处外部状态设置。
步骤3:工具诊断
- 输入:已明确的怀疑目标
Order和OrderService。 - 操作:
- 编写针对性的 ArchUnit 测试:
@Test public void order_entity_should_not_be_anemic() { JavaClasses classes = new ClassFileImporter().importPackages("com.ecommerce.order.domain"); ArchRule rule = classes().that().areAnnotatedWith(Entity.class) .and().resideInAPackage("..order..") .should(haveAtLeastOneBusinessMethod()); rule.check(classes); } - 运行
mvn test -Dtest=ArchitectureTest#order_entity_should_not_be_anemic,输出违规信息。 - 使用 jQAssistant 分析
Order的方法调用关系:
发现除了 getter/setter 外,仅有MATCH (type:Type {name: "Order"})-[:DECLARES]->(method:Method) RETURN method.name, method.signaturetoString、equals、hashCode。
- 编写针对性的 ArchUnit 测试:
- 输出:违规实体类清单,证实
Order实体无业务方法。
步骤4:根因确认
- 输入:工具诊断结果,系列前文知识库。
- 操作:对照本系列第 2 篇《战术 DDD》中聚合根不变量保护原则——“聚合根通过行为方法封装状态变更,保证业务不变量不被外部破坏”。确认
Order作为聚合根,其状态修改逻辑全部外泄,违反该原则。 - 输出:根因——聚合根不变量保护缺失,导致贫血模型。
步骤5:方案修正
- 输入:根因,领域模型设计知识。
- 操作:
- 重构代码:将支付、取消等行为方法移入
Order实体,setter 设为private。 - 编写防止复发的 ArchUnit 规则(禁止应用层调用 setter):
@Test public void application_services_should_not_call_setters_on_aggregates() { JavaClasses classes = new ClassFileImporter().importPackages("com.ecommerce.order"); ArchRule rule = noClasses().that().resideInAPackage("..application..") .should().callMethodWhere( target(nameMatching("set.*")) .and(target(owner(assignableTo(Order.class))))); rule.check(classes); } - 提交 PR,通过代码评审。
- 重构代码:将支付、取消等行为方法移入
- 输出:修正代码,防复发规则。
步骤6:验证恢复
- 输入:重构后的代码,CI 流水线。
- 操作:
- 运行全量单元测试和回归测试,全部通过。
- SonarQube 扫描
OrderService圈复杂度降至 30,技术债务比降至 2.1%。 - ArchUnit 测试套件通过。
- 架构评审委员会确认“反模式 #001 贫血模型”已关闭。
- 输出:反模式关闭单,更新架构治理看板。
小结:六步排查法通过“数据驱动、原则对照、工具固证、闭环验证”,将 DDD 反模式治理从一次性手动修复转变为可持续的工程实践。后续所有反模式剖析均遵循此六步法。
2. 12+ 反模式逐一剖析(上):领域模型腐化
领域模型腐化是最常见的 DDD 退化形式,通常表现为实体退化为纯粹的数据容器、聚合边界膨胀、资源库变成杂乱查询箱、值对象错当实体等。它们直接违背了战术 DDD 的聚合根不变量保护、小聚合优先、实体与值对象区分等核心原则。
反模式 1:贫血模型
违反原则:战术 DDD 聚合根不变量保护(第 2 篇)、陈述式代码行为封装(第 13 篇) 影响级别:L1(立即修复)
错误示例(before)
// 订单实体:只有属性,没有行为
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNo;
private String status; // PENDING, PAID, SHIPPED, CANCELLED
private BigDecimal totalAmount;
private Long buyerId;
private LocalDateTime createdAt;
// 省略所有 getter/setter,共计20+个
// 无任何自定义业务方法
}
// 应用服务层:充斥过程式代码
@Service
@Transactional
public class OrderApplicationService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentGateway paymentGateway;
public void payOrder(Long orderId, BigDecimal amount) {
Order order = orderRepository.findById(orderId).orElseThrow();
if (!"PENDING".equals(order.getStatus())) {
throw new BusinessException("只能支付待处理订单");
}
if (order.getTotalAmount().compareTo(amount) != 0) {
throw new BusinessException("支付金额不匹配");
}
order.setStatus("PAID");
orderRepository.save(order);
paymentGateway.charge(amount);
}
public void cancelOrder(Long orderId, String reason) {
Order order = orderRepository.findById(orderId).orElseThrow();
if (!"PENDING".equals(order.getStatus())) {
throw new BusinessException("只能取消待处理订单");
}
order.setStatus("CANCELLED");
// 缺少记录取消原因
orderRepository.save(order);
}
}
// 另一个Service中又出现直接 setStatus
@Service
public class ShipmentService {
public void shipOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
if (!"PAID".equals(order.getStatus())) {
throw new BusinessException("只能发货已支付订单");
}
order.setStatus("SHIPPED");
orderRepository.save(order);
}
}
故障现象
- 状态散落:
setStatus("PAID")、setStatus("CANCELLED")等调用分散在OrderApplicationService、ShipmentService、AdminOrderService等多个类中。 - 校验重复:每个 Service 方法开头都拷贝了相同的状态判断逻辑,一旦规则变更(如允许取消已支付订单),需要修改多处。
- 遗漏风险:某次修改中,
cancelOrder方法遗漏了取消原因记录,导致客户投诉。 - 测试困难:由于逻辑分散,单元测试需要 mock 多个 Service,且无法单独验证聚合的不变量。
排查思路(六步法应用)
- 现象描述:订单上下文
Order实体无业务方法,所有状态修改通过setStatus直接赋值,出现在 4 个 Service 文件中。 - 指标定位:ArchUnit 初步扫描显示
Order实体的公有方法中 getter/setter 占比 100%,业务方法数为 0;SonarQube 显示OrderApplicationService圈复杂度 87。 - 工具诊断:
- 执行针对性 ArchUnit 测试(代码见前文),输出违规:
Architecture Violation: Class <com.ecommerce.order.domain.Order> has no business methods - 运行
grep -rn "setStatus" src/main/java/com/ecommerce/order/,统计到 12 处调用。
- 执行针对性 ArchUnit 测试(代码见前文),输出违规:
- 根因确认:对照第 2 篇“聚合根必须通过行为方法保护不变量”,确认根因为聚合根不变量保护缺失。
- 方案修正:重构实体,封装业务行为。
- 验证恢复:回归测试通过,ArchUnit 套件通过,SonarQube 技术债务降低。
修正方案(after)
// 领域实体:封装状态变更
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNo;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private Money totalAmount; // 使用值对象
private Long buyerId;
private LocalDateTime createdAt;
private String cancelReason; // 只有取消时赋值
// 构造器,工厂方法等...
// 业务方法:支付
public void pay(Money paidAmount) {
if (this.status != OrderStatus.PENDING) {
throw new OrderDomainException("只能支付待处理订单,当前状态:" + this.status);
}
if (!this.totalAmount.equals(paidAmount)) {
throw new OrderDomainException("支付金额不匹配");
}
this.status = OrderStatus.PAID;
registerEvent(new OrderPaidEvent(this.id, paidAmount));
}
// 业务方法:取消
public void cancel(String reason) {
if (this.status != OrderStatus.PENDING) {
throw new OrderDomainException("只能取消待处理订单");
}
if (reason == null || reason.trim().isEmpty()) {
throw new OrderDomainException("取消原因不能为空");
}
this.status = OrderStatus.CANCELLED;
this.cancelReason = reason;
registerEvent(new OrderCancelledEvent(this.id, reason));
}
// 业务方法:发货(只能由已支付状态变更)
public void ship() {
if (this.status != OrderStatus.PAID) {
throw new OrderDomainException("只能发货已支付订单");
}
this.status = OrderStatus.SHIPPED;
registerEvent(new OrderShippedEvent(this.id));
}
// getter 保留(JPA 需要),但 setter 设为 protected/private
public OrderStatus getStatus() { return status; }
protected void setStatus(OrderStatus status) { this.status = status; } // 仅 JPA 使用
// ...
// 领域事件注册
private List<Object> domainEvents = new ArrayList<>();
private void registerEvent(Object event) { domainEvents.add(event); }
public List<Object> getDomainEvents() { return domainEvents; }
}
// 应用服务简化为调用聚合根方法
@Service
@Transactional
public class OrderApplicationService {
public void payOrder(Long orderId, Money amount) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.pay(amount); // 一行调用
// 事件由Spring Data发布
}
}
最佳实践与防复发
- 设计层面:在实体设计阶段,以业务行为作为接口契约,禁止在应用层直接修改聚合字段。
- 代码评审:CR 时检查实体类是否只有 getter/setter,如有则必须提供业务方法。
- 自动化规则:将贫血模型检测 ArchUnit 规则加入 CI 流水线;利用 Checkstyle 禁止
setStatus等名称(通过正则排除 JPA setter 是更细粒度做法)。 - 团队共识:定期召开“领域模型评审”,用统一语言对照代码。
反模式 2:聚合过大
违反原则:聚合设计小聚合优先原则(第 8 篇) 影响级别:L1
错误示例(before)
// 库存聚合:一个聚合包含仓库、库存项、库存日志,形成巨树
@Entity
public class Inventory {
@Id private Long id;
private String name;
@OneToMany(mappedBy = "inventory", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Warehouse> warehouses; // 多个仓库,仓库又有地址等
@OneToMany(mappedBy = "inventory", cascade = CascadeType.ALL)
private List<StockItem> items; // 几十种商品库存
@OneToMany(mappedBy = "inventory", cascade = CascadeType.ALL)
private List<InventoryLog> logs; // 日志巨大
@Version
private Long version;
// ...
}
@Entity
public class Warehouse {
@Id private Long id;
private String code;
@Embedded private Address address; // 地址修改也会锁整个Inventory
@ManyToOne private Inventory inventory;
// ...
}
故障现象
- 性能瓶颈:加载
Inventory需执行 10+ 次 JOIN,库存查询接口 P99 超过 2 秒。 - 乐观锁冲突频繁:修改仓库地址与扣减库存(修改
StockItem)冲突,冲突率高达 15%,大量重试影响业务。 - 事务边界过大:一次
@Transactional操作无意中锁定了整个库存聚合,导致其他操作阻塞。 - 拆分困难:当需要微服务拆分时,这个大聚合意味着必须整体迁移,无法独立部署。
排查思路
- 现象描述:库存模块,
Inventory聚合直接引用了Warehouse、StockItem、InventoryLog等 10+ 实体,关联了 8 张数据库表。 - 指标定位:开启
logging.level.org.hibernate.SQL=DEBUG,观察到单次findById产生了 12 条 SQL。数据库监控显示乐观锁冲突率 15%。 - 工具诊断:
- jQAssistant 查询实体引用:
返回 3 个MATCH (inventory:Type {name: "Inventory"})-[:DECLARES]->(field:Field)-[:HAS_TYPE]->(type) WHERE type.name STARTS WITH "List" RETURN field.name, type.name@OneToMany集合。 - ArchUnit 规则统计聚合大小:
ArchRule maxAggregateSize = classes().that().areAnnotatedWith(Entity.class) .should(new ArchCondition<JavaClass>("have reasonable number of relationships") { @Override public void check(JavaClass javaClass, ConditionEvents events) { long oneToManyCount = javaClass.getAllFields().stream() .filter(f -> f.isAnnotatedWith(OneToMany.class)).count(); if (oneToManyCount > 2) { events.add(SimpleConditionEvent.violated(javaClass, "聚合过大: " + oneToManyCount + " 个 @OneToMany")); } } });
- jQAssistant 查询实体引用:
- 根因确认:违反第 8 篇“小聚合优先,一次事务只修改一个聚合实例”原则。仓储地址变更与库存扣减属于不同的业务不变集,不应捆绑在同一聚合中。
- 方案修正:按写入足迹拆分聚合,通过 ID 引用和领域事件协作。
- 验证恢复:拆分后 SQL 减少到 3 条,乐观锁冲突率降至 2%,接口响应恢复正常。
修正方案(after)
// 库存聚合:仅保留核心库存项
@Entity
public class Inventory {
@Id private Long id;
private String name;
@OneToMany(mappedBy = "inventory", cascade = CascadeType.ALL, orphanRemoval = true)
private List<StockItem> items; // 仍然属于聚合,因为与库存总量强相关
// 仓库不再是聚合的一部分,仅保留ID引用
private Long warehouseId;
// 方法:扣减库存
public void deduct(Long productId, int quantity) { ... }
}
// 仓库独立聚合
@Entity
public class Warehouse {
@Id private Long id;
private String code;
@Embedded private Address address;
public void changeAddress(Address newAddress) { this.address = newAddress; }
}
// 库存日志作为独立聚合
@Entity
public class InventoryLog {
@Id private Long id;
private Long inventoryId; // 仅引用
private String operation;
// ...
}
// 应用服务:扣库存与仓库操作分离
@Service
public class InventoryService {
public void transferWarehouse(Long inventoryId, Long newWarehouseId) {
Inventory inv = invRepo.findById(inventoryId);
Warehouse wh = whRepo.findById(newWarehouseId);
inv.relocateTo(newWarehouseId);
wh.assignInventory(inventoryId); // 各自独立事务
}
}
最佳实践
- 定期进行“聚合尸检”:审查每个聚合的 @OneToMany/@ManyToOne 数量,超过 3 个时进行拆分评估。
- 使用“写入足迹分析法”:找出哪些对象在同一个业务操作中总是一起变化,它们才属于同一聚合。
- 通过 ArchUnit 规则自动检测聚合关系规模,纳入 CI。
反模式 3:资源库接口方法膨胀
违反原则:战术 DDD 资源库职责单一(第 2 篇) 影响级别:L2
错误示例
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByStatus(String status);
List<Order> findByStatusAndBuyerId(String status, Long buyerId);
List<Order> findByBuyerIdAndCreatedAtBetween(Long buyerId, LocalDateTime from, LocalDateTime to);
Page<Order> findByStatusOrderByCreatedAtDesc(String status, Pageable pageable);
// ... 25+ 个查询方法
}
故障现象
- 接口文件长达 200 行,方法名冗长。
- 每次新需求都增加一个
findByXxx,不同开发者添加的方法互相重叠。 - 无法支持动态组合查询(如按状态、金额范围、时间组合),只能不断加方法。
排查思路
grep -c "findBy\|findAllBy\|queryBy" OrderRepository.java
# 输出 25
根因确认
违反第 2 篇资源库“只提供通过主键查询及必要的规约查询”原则。资源库不应成为查询工厂。
修正方案
采用 Specification 模式(第 13 篇)或 CQRS 分离。
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
// 仅保留必须的聚合级查询
Optional<Order> findByOrderNo(String orderNo);
}
// 规约组合
Specification<Order> spec = Specification.where(OrderSpecs.byStatus(OrderStatus.PAID))
.and(OrderSpecs.byBuyerId(buyerId))
.and(OrderSpecs.byDateRange(from, to));
List<Order> orders = orderRepository.findAll(spec);
最佳实践
- 监控 Repository 方法数量,超过 8 个时触发告警,考虑引入 Specification 或专用 QueryService。
- 利用 CQRS 的读模型(第 4 篇)应对复杂查询,避免写模型资源库膨胀。
反模式 8:值对象被错当实体
违反原则:战略设计实体与值对象区分(第 1 篇) 影响级别:L2
错误示例
@Entity
public class Address {
@Id @GeneratedValue
private Long id;
private String province;
private String city;
private String street;
// getters and setters
}
@Entity
public class User {
@Id private Long id;
@ManyToOne
private Address address; // 作为实体关联
}
故障现象
- 修改用户地址时可能意外创建了新的
Address行,导致孤儿数据。 - 无法通过值相等比较是否为同一地址,需要查询数据库。
- 地址没有独立生命周期的概念。
排查思路
ArchUnit 规则检测 @Entity 类中是否包含 province、city、street 等典型值语义组合。
classes().that().areAnnotatedWith(Entity.class)
.and().containAnyFieldThat(hasName("province"))
.should().notBeAnnotatedWith(Entity.class); // 期望不是Entity
根因确认
违反第 1 篇值对象定义:无业务标识,由属性值决定相等性。地址应建模为 @Embeddable。
修正方案
@Embeddable
public class Address {
private String province;
private String city;
private String street;
// 无参构造器(JPA要求),全参构造器
// 实现 equals/hashCode 基于所有字段
// 不提供 setter,设计为不可变
}
最佳实践
检查所有 @Entity,识别那些没有独立标识、靠属性组合描述的概念,改用 @Embeddable。利用 ArchUnit 规则禁止 @Entity 包含特定值模式字段。
反模式 11:未遵循小聚合,事务边界模糊
违反原则:聚合设计“一次事务只修改一个聚合”(第 8 篇) 影响级别:L3
错误示例
@Transactional
public void confirmOrder(Long orderId) {
Order order = orderRepo.findById(orderId);
order.confirm();
Payment payment = paymentRepo.findByOrderId(orderId);
payment.markPaid();
Inventory inventory = inventoryRepo.findByProductId(...);
inventory.deduct(quantity);
Shipment shipment = new Shipment(order);
shipmentRepo.save(shipment);
}
一个事务修改了 Order、Payment、Inventory、Shipment 四个聚合。
故障现象
事务边界过大,锁范围宽,乐观锁重试率高;未来拆分微服务时不得不采用分布式事务,复杂度剧增。
排查思路
使用 ArchUnit 规则:classes().that().resideInAPackage("..application..").and().areAnnotatedWith(Service.class) 的方法中调用的 Repository save/delete 数量超过 1 个。
根因确认
违反第 8 篇聚合事务边界原则。
修正方案
利用领域事件实现最终一致性(第 5 篇):
// Order应用服务
@Transactional
public void confirmOrder(Long orderId) {
Order order = orderRepo.findById(orderId);
order.confirm(); // 内部发布 OrderConfirmedEvent
}
// 各个消费者
@Component
public class PaymentEventHandler {
@Transactional
@EventListener
public void on(OrderConfirmedEvent event) {
Payment payment = paymentRepo.findByOrderId(event.getOrderId());
payment.markPaid();
}
}
// Inventory, Shipment 同理
最佳实践
严格遵循“一个事务一个聚合”原则,除非强一致性要求且聚合划分设计有误,否则所有跨聚合操作均通过事件异步处理。
3. 12+ 反模式逐一剖析(中):架构边界腐化
反模式 5:缺少防腐层 ACL
违反原则:防腐层 ACL 隔离外部模型(第 7 篇) 影响级别:L1
错误示例
// 支付领域服务直接依赖外部DTO
@Service
public class PaymentDomainService {
@Autowired
private PaymentGatewayFeignClient paymentClient;
public void processPayment(Order order) {
ExternalPaymentResponse resp = paymentClient.charge(
new ExternalPaymentRequest(order.getTotalAmount()));
if ("SUCCESS".equals(resp.getResultCode())) {
order.pay(new Money(resp.getAmount()));
}
}
}
故障现象
外部支付网关升级,resultCode 字段改为 status,ExternalPaymentResponse 结构大变,导致 PaymentDomainService 等多处抛出 NullPointerException,修改波及数十个文件。
排查思路
ArchUnit 规则:
classes().that().resideInAPackage("..domain..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage("..domain..", "..shared..", "java..", "javax..");
检测到 payment.domain 依赖了 com.external.payment.gateway.dto.ExternalPaymentResponse。
根因确认
违反第 7 篇防腐层设计原则。
修正方案
建立 PaymentPort 领域接口 + PaymentAdapter 基础设施适配器 + 模型转换。
// 领域层端口
public interface PaymentPort {
PaymentConfirmation pay(Money amount, String orderNo);
}
// 基础设施层适配器
@Component
public class PaymentAdapter implements PaymentPort {
@Autowired
private PaymentGatewayFeignClient feignClient;
public PaymentConfirmation pay(Money amount, String orderNo) {
ExternalPaymentRequest req = toExternal(amount, orderNo);
ExternalPaymentResponse resp = feignClient.charge(req);
return toDomain(resp);
}
// 翻译方法
}
最佳实践
所有外部系统(REST API、消息队列、第三方库)都必须通过端口/适配器接入,领域层不得 import 外部 DTO。通过 ArchUnit 规则严格执行。
反模式 6:跨界上下文直调
违反原则:模块化单体边界约束(第 3 篇) 影响级别:L2
错误示例
// 订单上下文的领域服务
@Service
public class OrderDomainService {
@Autowired
private InventoryRepository inventoryRepository; // 库存模块的仓储
public void placeOrder(Order order) {
inventoryRepository.deductStock(...);
orderRepository.save(order);
}
}
故障现象
订单模块直接操作库存数据库表,包依赖图混乱。当库存模块重构数据库时,订单模块被迫修改。
排查思路
ArchUnit 包规则:
classes().that().resideInAPackage("..order..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage("..order..", "..shared..", "java..");
违规列表包含 InventoryRepository。
根因确认
违反第 3 篇模块化单体边界。
修正方案
定义 InventoryPort 接口在 order 领域层,由 inventory 模块实现。订单上下文仅依赖接口。
最佳实践
每个限界上下文的 domain 包作为 API 暴露端口,实现模块放在基础设施或适配器包,通过依赖注入绑定。
反模式 4:领域事件过细/过粗
违反原则:领域事件语义化与粒度(第 5 篇) 影响级别:L2
错误示例
- 过粗:
OrderUpdated包含订单所有字段,消费者需判断哪些字段变了。 - 过细:
OrderCreated、OrderItemAdded、OrderTotalCalculated等,每个字段变都发一个事件。
故障现象
消费方代码复杂且易出错,或消息中间件积压无意义事件。
排查思路
- 统计事件类型数量与聚合行为方法比例。
- 统计事件平均字段数,超过 20 则过粗。
- 查看消费者代码是否存在大量
if (event.getXxx() != null)判断。
根因确认
违反第 5 篇“一个业务动作对应一个领域事件,事件携带必要的业务含义”。
修正方案
调整为 OrderPlaced、OrderPaid、OrderCancelled 等业务事件,每个事件只含标识和关键业务属性。
最佳实践
事件命名采用“聚合名+过去式动词”,只携带完成后续流程所需的最少数据。
反模式 9:领域事件未幂等
违反原则:领域事件消费幂等(第 5 篇) 影响级别:L2
错误示例
@EventListener
public void handleOrderPaid(OrderPaidEvent event) {
// 直接执行业务,未检查 eventId
sendSms(event.getBuyerPhone(), "支付成功");
}
故障现象
消息重试导致用户收到多条重复短信,库存重复扣减。
排查思路
检查是否存在 processed_events 表,消费者是否有 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE 逻辑。
修正方案
@EventListener
@Transactional
public void handleOrderPaid(OrderPaidEvent event) {
if (processedEventRepo.existsByEventId(event.getEventId())) {
log.warn("重复事件忽略: {}", event.getEventId());
return;
}
// 业务处理
processedEventRepo.save(new ProcessedEvent(event.getEventId()));
}
最佳实践
所有事件消费者必须实现幂等,可借助数据库唯一约束或 Redis SETNX。
反模式 10:CQRS 过度设计
违反原则:CQRS 适用场景判断(第 4 篇) 影响级别:L3
错误示例
用户信息的简单增删改查也通过命令+事件同步到 Elasticsearch 读库,查询直接从 ES 出。
故障现象
引入不必要的延迟和运维复杂度,但并无复杂查询或高并发的收益。
排查思路
评估读模型是否真的需要:如果只是简单的按 ID 查询或分页列表,且数据量与并发不大,无需 CQRS。
修正方案
移除读写分离,直接使用 JPA Repository 查询。仅在报表、全文搜索等场景保留 CQRS。
最佳实践
CQRS 是架构工具而非默认选项,使用前需明确收益。
4. 12+ 反模式逐一剖析(下):代码表达腐化
反模式 7:没有统一语言
违反原则:战略设计统一语言(第 1 篇) 影响级别:L3
错误示例
- 类名:
OrderInfo、OrderDTO、OrderVO - 方法:
updateOrderStatus、createOrder - 字段:
user_id但业务术语称“买家”
故障现象
领域专家看不懂代码,沟通成本高。一个概念多种表达,代码可读性差。
排查思路
Checkstyle 命名规则禁止类名后缀 Info|DTO|VO;SonarQube 检测 CRUD 风格方法名;人工对照领域术语表审查。
根因确认
违反第 1 篇统一语言原则。
修正方案
统一为 Order 实体,方法 confirmPayment()、placeOrder(),字段 buyerId。代码重命名后通过 IDE 全局替换,确保一致性。
最佳实践
从事件风暴输出“术语表”,将术语表与 Checkstyle、SonarQube 规则联动,定期扫描代码中不符合术语的命名。
反模式 12:陈述式代码退化为命令式
违反原则:陈述式代码行为封装(第 13 篇) 影响级别:L3
错误示例
重构后的 order.confirmPayment() 被绕过,又在 Service 中直接 order.setStatus(PAID); orderRepository.save(order);
故障现象
行为方法被架空,状态转换约束失效,历史问题重现。
排查思路
通过 ArchUnit 扫描应用层是否直接调用聚合根的 setXxx 方法;SonarQube 检测 setStatus 的调用位置(排除 JPA 内部调用)。
根因确认
违反第 13 篇陈述式代码原则:以行为方法表达业务意图,禁止绕过。
修正方案
将聚合根的所有 setter 设为 protected(JPA 可通过字段注入)。编写 ArchUnit 规则阻断,代码评审强调。
最佳实践
在实体设计时,通过编码规范强制 setter 私有化(或包级可见),仅通过构造函数/工厂方法创建,行为方法修改状态。
12+ 反模式分类全景图(具体图因篇幅略,内容与之前类似,但可添加影响级和篇章引用):
| 腐化域 | 反模式 | 错误示例 | 违反原则 | 排查工具 | 修正方案 | 级别 |
|---|---|---|---|---|---|---|
| 领域模型 | 贫血模型 | Entity 仅 getter/setter | 第2篇聚合根保护 | ArchUnit hasBusinessMethods() | 封装行为方法 | L1 |
| 领域模型 | 聚合过大 | @OneToMany 10+ | 第8篇小聚合 | jQAssistant, SQL日志 | 拆分聚合 | L1 |
| 领域模型 | 资源库膨胀 | 25个findBy方法 | 第2篇资源库职责 | grep统计方法数 | Specification/CQRS | L2 |
| 领域模型 | 值对象误用 | Address用@Entity | 第1篇值对象 | ArchUnit字段模式 | @Embeddable | L2 |
| 领域模型 | 事务边界模糊 | 一次事务多聚合 | 第8篇一次一聚合 | ArchUnit跨Repository | 领域事件 | L3 |
| 架构边界 | 缺少ACL | 直接依赖外部DTO | 第7篇防腐层 | ArchUnit包依赖 | Port/Adapter | L1 |
| 架构边界 | 跨上下文直调 | 订单调库存Repository | 第3篇模块边界 | ArchUnit包依赖 | Port接口 | L2 |
| 架构边界 | 事件粒度不当 | 事件过粗/过细 | 第5篇事件设计 | 统计字段、类型数 | 调整粒度 | L2 |
| 架构边界 | 事件未幂等 | 直接消费未去重 | 第5篇幂等 | 检查去重表 | processed_events | L2 |
| 架构边界 | CQRS过度设计 | 简单CRUD也分离 | 第4篇CQRS适用 | 评估查询复杂度 | 回退直接查询 | L3 |
| 代码表达 | 统一语言缺失 | OrderInfo/DTO命名 | 第1篇统一语言 | Checkstyle/SonarQube | 术语表对齐 | L3 |
| 代码表达 | 陈述式退化 | 绕过行为方法setStatus | 第13篇陈述式 | ArchUnit setter调用 | 行为方法私有setter | L3 |
5. 三大腐化域的排查决策树
本节提供可直接在架构评审中使用的决策树,每张决策树对应一个腐化域。
5.1 领域模型腐化决策树
flowchart TB
Start["表象:实体/聚合设计疑问"] --> Q1{"实体是否只有getter/setter?"}
Q1 -- "是" --> Anemic["反模式1:贫血模型"]
Q1 -- "否" --> Q2{"聚合内@OneToMany数量>3?"}
Q2 -- "是" --> LargeAgg["反模式2:聚合过大"]
Q2 -- "否" --> Q3{"Repository查询方法>10?"}
Q3 -- "是" --> RepoBloted["反模式3:资源库膨胀"]
Q3 -- "否" --> Q4{"@Entity标识明显为值语义?"}
Q4 -- "是" --> VOEntity["反模式8:值对象误用"]
Q4 -- "否" --> Q5{"一个@Transactional方法操作多个聚合?"}
Q5 -- "是" --> WideTx["反模式11:事务边界模糊"]
Q5 -- "否" --> Healthy["领域模型基本健康"]
Anemic --> Fix1["修正:将业务逻辑移入聚合根,提供行为方法"]
LargeAgg --> Fix2["修正:按写入足迹拆分聚合,外键ID引用"]
RepoBloted --> Fix3["修正:引入Specification/CQRS"]
VOEntity --> Fix4["修正:改用@Embeddable不可变设计"]
WideTx --> Fix5["修正:发布领域事件实现最终一致性"]
classDef default fill:#f1f5f9,stroke:#334155,stroke-width:1.5px,color:#1e293b
classDef decision fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
class Q1,Q2,Q3,Q4,Q5 decision
图表说明:
- 主旨概括:通过五个判断节点快速导航到具体的领域模型反模式,并直接链接到修正方案。
- 逐层/逐元素分解:节点 Q1 检查行为封装;Q2 度量聚合大小;Q3 检查 Repository 膨胀;Q4 关注值对象标识误用;Q5 评审事务边界。每个叶子节点输出反模式及修正动作。
- 设计原理映射:决策树根植于战术 DDD 的聚合设计、资源库职责和值对象概念,违反即产生对应反模式。
- 工程联系与关键结论:将该决策树转化为自动化脚本,可在 CI 中对每个模块进行评分,实现架构健康度量化。
5.2 架构边界腐化决策树
flowchart TD
Start["表象:跨模块/外部依赖疑问"] --> Q1{"domain包是否直接import外部DTO?"}
Q1 -- "是" --> NoACL["反模式5:缺少ACL"]
Q1 -- "否" --> Q2{"是否跨上下文直接注入Repository?"}
Q2 -- "是" --> DirectCall["反模式6:跨界直调"]
Q2 -- "否" --> Q3{"领域事件类型数与聚合行为比<0.5?"}
Q3 -- "是" --> CoarseEvent["反模式4a:事件过粗"]
Q3 -- "否" --> Q4{"事件类平均字段>20? 或事件类型过多?"}
Q4 -- "是" --> FineEvent["反模式4b:事件过细"]
Q4 -- "否" --> Q5{"事件消费者是否有幂等处理?"}
Q5 -- "否" --> NonIdemp["反模式9:事件未幂等"]
Q5 -- "是" --> Q6{"是否对简单实体应用CQRS?"}
Q6 -- "是" --> OverCQRS["反模式10:CQRS过度"]
Q6 -- "否" --> Healthy["架构边界健康"]
NoACL --> Fix1["修正:建立Port+Adapter+Translator"]
DirectCall --> Fix2["修正:定义领域Port接口,实现依赖倒置"]
CoarseEvent --> Fix3["修正:细化为业务动作事件"]
FineEvent --> Fix4["修正:合并为粗粒度业务事件"]
NonIdemp --> Fix5["修正:引入processed_events去重表"]
OverCQRS --> Fix6["修正:简单场景回退到直接查询"]
classDef default fill:#f1f5f9,stroke:#334155,stroke-width:1.5px,color:#1e293b
classDef decision fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
class Q1,Q2,Q3,Q4,Q5,Q6 decision
图表说明:
- 主旨概括:以依赖方向、事件设计、集成模式为线索,诊断架构边界腐化的六大反模式。
- 逐层/逐元素分解:从防腐层缺失、上下文直调开始,再到事件粒度和幂等,最后到 CQRS 滥用,形成逐步深入的排查路径。
- 设计原理映射:对应第 7 篇 ACL、第 3 篇模块化边界、第 5 篇事件设计、第 4 篇 CQRS 适用性。
- 工程联系与关键结论:大部分边界腐化可以用 ArchUnit 的包依赖规则自动化发现,而事件设计问题需结合统计分析和人工评审。
5.3 代码表达腐化决策树
flowchart TD
Start["表象:代码难读/术语混乱"] --> Q1{"类名是否使用Info/DTO/VO后缀?"}
Q1 -- "是" --> Lang["反模式7:统一语言缺失"]
Q1 -- "否" --> Q2{"方法名是否包含update/create等CRUD?"}
Q2 -- "是" --> Lang
Q2 -- "否" --> Q3{"是否存在绕过聚合方法直接setStatus?"}
Q3 -- "是" --> DeclDecline["反模式12:陈述式退化"]
Q3 -- "否" --> Q4{"Repository查询方法数>15?"}
Q4 -- "是" --> DeclDecline2["反模式3:资源库膨胀"]
Q4 -- "否" --> Healthy["代码表达健康"]
Lang --> Fix1["修正:按术语表重命名,配置Checkstyle"]
DeclDecline --> Fix2["修正:setter私有化,ArchUnit禁止外部调用"]
DeclDecline2 --> Fix3["修正:Specification/QueryService"]
classDef default fill:#f1f5f9,stroke:#334155,stroke-width:1.5px,color:#1e293b
classDef decision fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
class Q1,Q2,Q3,Q4 decision
图表说明:
- 主旨概括:从命名约定、行为封装和接口膨胀三个维度快速定位代码表达问题。
- 逐层/逐元素分解:通过 Checkstyle 和 ArchUnit 可自动化检测大部分节点。
- 设计原理映射:第 1 篇统一语言、第 13 篇陈述式代码、第 2 篇资源库职责。
- 工程联系与关键结论:代码表达腐化直接影响团队沟通效率和维护成本,应优先通过静态检查自动拦截。
6. 生产诊断工具链实战
诊断工具链是实现“防复发”的关键。本章提供完整的 CI/CD 集成方案和典型命令。
6.1 工具链在 CI/CD 中的集成位置
flowchart TD
Git[代码提交] --> CS[Checkstyle 10.x 命名规范]
CS --> SQ[SonarQube 9.x 质量门禁]
SQ --> AU[ArchUnit 0.23.x 架构测试]
AU --> SC[Spring Cloud Contract 3.1.x 契约测试]
SC --> Deploy[部署到测试环境]
SQ -- 技术债务/贫血模型报告 --> Dashboard[架构治理看板]
AU -- 边界违规报告 --> Dashboard
SC -- 契约兼容性报告 --> Dashboard
图表说明:
- 主旨概括:在持续集成流水线中,按静态分析、架构测试、契约测试的顺序构建多层质量防线。
- 逐层/逐元素分解:Checkstyle 负责代码风格和命名;SonarQube 深度扫描代码质量;ArchUnit 执行架构规则;Spring Cloud Contract 验证 API 兼容性。所有结果汇总到看板。
- 设计原理映射:体现了《持续交付》中质量内建的思想,将架构决策代码化。
- 工程联系与关键结论:所有检测规则均作为测试代码的一部分存储在源码仓库,实现“架构即代码”。任何违反都会导致构建失败,强制修复。
6.2 ArchUnit 规则体系
以下为完整的 ArchUnit 测试类,涵盖贫血模型、跨上下文依赖、命名规范等。
@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(packages = "com.ecommerce")
public class DDDArchitectureTest {
// 1. 贫血模型检测:领域实体必须有业务方法
@ArchTest
public static final ArchRule entities_should_have_business_methods =
classes().that().resideInAPackage("..domain..")
.and().areAnnotatedWith(Entity.class)
.should(new ArchCondition<JavaClass>("have business methods") {
@Override
public void check(JavaClass javaClass, ConditionEvents events) {
boolean hasBusinessMethod = javaClass.getMethods().stream()
.filter(m -> m.getModifiers().contains(JavaModifier.PUBLIC))
.anyMatch(m -> !m.getName().startsWith("get")
&& !m.getName().startsWith("set")
&& !m.getName().startsWith("is")
&& !m.getName().equals("toString")
&& !m.getName().equals("equals")
&& !m.getName().equals("hashCode"));
if (!hasBusinessMethod) {
events.add(SimpleConditionEvent.violated(javaClass,
"贫血模型: 无业务方法"));
}
}
});
// 2. 跨上下文直调检测:订单模块不得依赖库存模块基础设施
@ArchTest
public static final ArchRule order_context_should_not_depend_on_inventory_infrastructure =
classes().that().resideInAPackage("..order..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage("..order..", "..shared..", "java..", "javax..",
"org.springframework..", "..domain..", "..application..");
// 3. 防腐层检测:domain 包不得直接依赖外部系统的 DTO
@ArchTest
public static final ArchRule domain_should_not_depend_on_external_dtos =
classes().that().resideInAPackage("..domain..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage("..domain..", "..shared..", "java..", "javax..");
// 4. 聚合根 setter 禁止被应用层调用
@ArchTest
public static final ArchRule application_should_not_call_setter_on_aggregates =
noClasses().that().resideInAPackage("..application..")
.should().callMethodWhere(
target(nameMatching("set.*"))
.and(target(owner(assignableTo(Order.class))))
);
// 5. Repository 方法数量检测(自定义条件)
@ArchTest
public static final ArchRule repository_interfaces_should_not_have_too_many_query_methods =
classes().that().areAnnotatedWith(Repository.class)
.should(new ArchCondition<JavaClass>("have <= 8 query methods") {
@Override
public void check(JavaClass javaClass, ConditionEvents events) {
long count = javaClass.getMethods().stream()
.filter(m -> m.getName().startsWith("find") || m.getName().startsWith("query"))
.count();
if (count > 8) {
events.add(SimpleConditionEvent.violated(javaClass,
"资源库膨胀: " + count + " 个查询方法"));
}
}
});
}
输出解读:当构建失败时,错误日志会清晰列出违反的类和规则,如 Class <com.ecommerce.order.domain.Order> does not satisfy have business methods。
6.3 SonarQube 质量门禁配置
sonar-project.properties 示例:
sonar.projectKey=ecommerce-ddd
sonar.projectName=DDD电商系统
sonar.java.source=8
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
# 质量门禁:阻断错误0,严重违规<5,覆盖率<80%失败
sonar.qualitygate.wait=true
SonarQube 自定义规则(通过 XML 或 UI 配置)可检测贫血模型趋势:当实体类没有 public 方法(排除 getter/setter)时标记为 “Anemic Entity”。
6.4 Spring Cloud Contract 契约验证
@SpringBootTest
@AutoConfigureStubRunner(ids = {"com.ecommerce:payment-service:+:stubs:8080"}, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class PaymentContractTest {
@Autowired
private PaymentPort paymentPort; // 适配器调用stub
@Test
public void should_pay_successfully() {
PaymentConfirmation confirm = paymentPort.pay(new Money("100.00"), "ORD-001");
assertThat(confirm.isSuccess()).isTrue();
}
}
任何支付 API 变更,如果破坏了消费者契约,该测试会失败,提前预警。
6.5 jQAssistant 分析
# 扫描项目
jqassistant.cmd scan -f target/classes
# 查询聚合大小
MATCH (type:Type)-[:ANNOTATED_BY]->(a:Annotation) WHERE a.name='Entity'
OPTIONAL MATCH (type)-[:DECLARES]->(field:Field)-[:HAS_TYPE]->(coll) WHERE coll.name CONTAINS 'List'
RETURN type.name, count(field) as oneToManyCount ORDER BY oneToManyCount DESC
7. AI 辅助代码审查的探索实践
引入 LLM 进行自动反模式扫描,作为人工审查的补充。
7.1 工作流程
flowchart TB
Dev[开发者提交PR] --> CI[CI构建]
CI --> AI[AI 审查服务]
AI -- 审查Prompt --> LLM[LLM 模型]
LLM -- 反模式报告草稿 --> Human[架构师复核]
Human -- 确认后 --> Jira[Jira 债务看板]
Human -- 误报标记 --> Feedback[反馈至Prompt优化]
Jira -- 纳入迭代 --> Sprint
图表说明:
- 主旨:将 AI 作为第一道扫描工具,快速生成疑似反模式列表,由人工确认后纳入治理流程。
- 逐层分解:CI 触发后调用 AI 服务,传入代码 diff 和 Prompt;LLM 返回结构化报告;架构师逐项核实真伪,排除误报,将真正问题加入 Jira。
- 设计原理映射:借鉴了静态分析工具的思路,但增加了自然语言理解和代码语义分析,弥补了规则引擎的不足。
- 工程联系:AI 不能替代人工判断,但可以大幅提升评审效率和覆盖面,尤其适用于大型项目。
7.2 Prompt 设计
你是一位 DDD 架构审计专家。请扫描以下 Java 代码(或 Git diff),检测并报告下列反模式:
1. 贫血模型:@Entity 类只有 getter/setter,没有任何业务方法(排除 toString, equals, hashCode)。
2. 聚合过大:@Entity 中使用 @OneToMany 关联的实体数量超过 5。
3. 资源库方法膨胀:Repository 接口的查询方法(findBy/queryBy)超过 10 个。
4. 缺少防腐层:domain 包中的类直接 import 了外部系统(非本限界上下文)的 DTO 类。
5. 领域事件粒度不当:事件类的字段数超过 20(过粗)或者事件类型数量与聚合行为方法数比例失衡(过细)。
6. 跨上下文直调:某个限界上下文的 domain 层直接依赖了另一个上下文的 Repository 或 Service。
7. 统一语言缺失:类名包含 Info、DTO、VO 等非业务后缀。
8. 值对象被错当实体:带有 @Entity 注解的类,其字段组合明显为值语义(如 province, city, street)。
9. 事件未幂等:事件监听器方法中没有检查事件是否已处理。
10. 陈述式代码退化:在 @Service 或应用层代码中直接调用聚合根的 setXxx 方法。
对于每个发现,请按以下 JSON 格式输出:
{
"反模式名称": "...",
"文件路径": "...",
"行号": "...",
"描述": "具体违反了哪条原则",
"建议修正": "..."
}
如果没有发现,输出空数组。
7.3 LLM 输出示例(部分)
[
{
"反模式名称": "贫血模型",
"文件路径": "order/domain/Order.java",
"行号": "10-60",
"描述": "Order 实体仅有 getter/setter,无业务方法,状态变更逻辑全部外泄。",
"建议修正": "将 pay、cancel 等方法移至 Order 内部,setStatus 设为 private。"
},
{
"反模式名称": "缺少防腐层",
"文件路径": "payment/domain/PaymentService.java",
"行号": "25",
"描述": "直接 import 了外部支付网关的 ExternalPaymentResponse DTO。",
"建议修正": "引入 PaymentPort 接口隔离,通过适配器转换。"
}
]
7.4 人工复核三原则
- 真实性验证:确认 AI 指出的问题确实存在,不是对工具类或适配器的误判。
- 排除误报:如
OrderInfo是遗留系统适配的过渡类,已计划移除,可标记为误报。 - 修正可行性:评估 AI 建议的修正方案是否符合当前架构演进方向,必要时调整。
将确认后的反模式创建 Jira Issue,并标记优先级 L1/L2/L3,纳入技术债务看板(微服务系列第 17 篇)。
8. 反模式关联推演:连锁反应链
反模式往往不是孤立存在,一个破窗效应会引发一连串架构退化。
连锁推演图
flowchart TD
subgraph Path1 [贫血模型连锁]
A1[贫血模型] --> A2[业务规则散落Service]
A2 --> A3[多个Service重复校验]
A3 --> A4[校验规则变更遗漏]
A4 --> A5[数据不一致/线上故障]
end
subgraph Path2 [聚合过大连锁]
B1[聚合过大] --> B2[加载性能下降]
B2 --> B3[应用层引入懒加载LAZY]
B3 --> B4[跨事务访问懒加载属性]
B4 --> B5[LazyInitializationException]
B5 --> B6[业务错误/用户投诉]
end
subgraph Path3 [缺少ACL连锁]
C1[缺少ACL] --> C2[外部模型变更]
C2 --> C3[领域层连锁修改]
C3 --> C4[领域模型污染]
C4 --> C5[限界上下文边界模糊]
C5 --> C6[跨上下文直调蔓延]
end
图表说明:
- 主旨概括:揭示一个微小反模式如何通过依赖链和后续妥协,最终引发严重的业务问题。
- 逐路径分解:路径1说明贫血模型导致代码重复,最终在变更时遗漏,造成数据不一致;路径2说明聚合过大迫使开发者引入懒加载,进而触发事务异常;路径3说明无防腐层导致外部变更直接污染领域,边界模糊后进一步引发跨上下文直调。
- 设计原理映射:体现了《实现领域驱动设计》中“模型完整性”和《Clean Code》中“破窗效应”的概念。
- 工程联系与关键结论:在排查一个反模式时,必须沿着依赖关系链向上追溯可能引发的次生问题,制定整体修复计划,而非头痛医头。
9. 贯穿案例:电商季度架构评审 12 项反模式排查实录
9.1 评审背景
电商系统基于模块化单体架构,运行一年,订单、库存、支付、物流四个限界上下文。第 4 季度架构评审启动,目标是评估 DDD 落地健康度。
9.2 评审执行
- 阶段一:自动化扫描。运行 SonarQube 全量扫描,ArchUnit 架构测试,Spring Cloud Contract 契约验证。
- 阶段二:AI 辅助初筛。将核心领域模块代码输入 LLM,生成初始反模式列表。
- 阶段三:人工代码走查。架构师团队基于扫描结果和 AI 报告,逐文件走查,对照术语表。
9.3 12 项反模式清单与分级
| 编号 | 反模式 | 上下文/模块 | 违反原则 | 级别 | 发现手段 |
|---|---|---|---|---|---|
| 1 | 贫血模型 | 订单 Order | 第2篇 | L1 | ArchUnit+AI |
| 2 | 聚合过大 | 库存 Inventory | 第8篇 | L1 | SQL日志+AI |
| 3 | 资源库膨胀 | 订单 OrderRepository | 第2篇 | L2 | grep统计 |
| 4 | 事件粒度过粗 | 物流 OrderUpdated | 第5篇 | L2 | 人工走查 |
| 5 | 缺少ACL | 支付 ExternalPayment | 第7篇 | L1 | ArchUnit包规则 |
| 6 | 跨上下文直调 | 通知 NotificationService | 第3篇 | L2 | ArchUnit包规则 |
| 7 | 统一语言缺失 | 物流 ShipmentInfo | 第1篇 | L3 | Checkstyle |
| 8 | 值对象误用 | 用户 Address | 第1篇 | L2 | ArchUnit+AI |
| 9 | 事件未幂等 | 通知消费者 | 第5篇 | L2 | 代码审查 |
| 10 | CQRS过度设计 | 用户信息查询 | 第4篇 | L3 | 架构评审 |
| 11 | 未遵循小聚合 | 订单 confirmOrder | 第8篇 | L3 | ArchUnit事务 |
| 12 | 陈述式退化 | 支付 setStatus | 第13篇 | L3 | SonarQube+AI |
9.4 修复时间线与过程
gantt
title 电商季度架构评审与修复时间线
dateFormat YYYY-MM-DD
section 评审与识别
架构评审会议 :milestone, 2024-01-15, 1d
自动化+AI扫描 :done, 2024-01-16, 2d
反模式清单输出 :done, 2024-01-18, 1d
section L1修复(立即)
#1 贫血模型修复 :active, 2024-01-19, 5d
#2 聚合过大拆分 :active, 2024-01-19, 8d
#5 防腐层建立 :active, 2024-01-19, 6d
section L2修复(下迭代)
#3 资源库膨胀重构 :2024-01-26, 5d
#6 直调与事件修复 :2024-01-30, 7d
#4/#8/#9 修复 :2024-02-01, 7d
section L3债务规划
#7/#10/#11/#12 修复 :2024-02-10, 10d
section 验收与防复发
工具链规则上线 :2024-02-20, 5d
季度回归评审 :milestone, 2024-03-01, 1d
图表说明:
- 主旨概括:展示从评审到全部反模式关闭的 2 个月治理周期,L1 优先,并行推进。
- 逐层/逐元素分解:L1 问题在三周内修复完毕;L2 跟进;L3 作为技术债排入后续迭代。最终工具链规则上线形成长期防线。
- 设计原理映射:体现了技术债务管理的四象限方法,将紧急重要度应用于架构治理。
- 工程联系与关键结论:通过这次集中治理,系统新增 12 条 ArchUnit 规则,SonarQube 质量门禁调整,团队对 DDD 反模式认知大幅提升,形成了可持续的架构守护机制。
9.5 修复示例:订单贫血模型重构(代码对比见前文反模式1),最终所有反模式关闭。
10. 与前后系列的衔接
本文是“领域驱动设计与业务架构”系列的收官之作,承担着将理论体系转化为反模式排查能力的任务。
- 关联前 14 篇:每个反模式都明确指出违反了哪一篇的原则,实现知识的闭环印证。例如贫血模型对接第 2 篇,聚合过大对接第 8 篇,等等。
- 衔接微服务系列第 17 篇《治理与标准化》:本文产出的反模式清单、ArchUnit 规则、SonarQube 门禁等,将直接作为治理工具输入,纳入企业技术债务管理平台。
- 衔接微服务系列第 20 篇《Spring 反模式》:本文将领域反模式的方法论铺垫好,第 20 篇将探讨 Spring 框架使用中的反模式(如滥用
@Transactional、配置混乱等),两者构成完整的反模式治理体系。
11. 面试高频专题(不少于 14 题)
11.1 什么是 DDD 中的贫血模型?如何通过 ArchUnit 检测和修复?
- 一句话回答:贫血模型指实体只有数据没有行为,业务逻辑外泄,违反聚合根封装;可通过 ArchUnit 检测实体是否有业务方法,修复是将行为移入实体。
- 详细解释:贫血模型(Anemic Domain Model)由 Martin Fowler 提出,实体沦为纯数据容器(只有 getter/setter),所有业务逻辑都写在 Service 中。这导致聚合的不变量无法被保证,重复代码滋生。检测手段:ArchUnit 规则扫描
@Entity类,检查公有方法中是否除 getter/setter/toString 外有业务方法;如果没有,则触发违规。修复方案:按照战术 DDD,将状态变更行为(如pay(),cancel())封装到实体内部,setter 改为私有,并通过领域事件通知外部。防复发可添加 ArchUnit 规则禁止应用层调用 setter。 - 多角度追问:
- 贫血模型和 Rich Model 的取舍场景?— 简单 CRUD 可适当放宽,但核心领域必须充血。
- 实体 setter 私有后,Spring Data JPA 如何工作?— JPA 可以通过字段注入或
@Access(AccessType.FIELD)绕过 setter。 - 如何说服团队从贫血向充血转型?— 从代码重复度、线上缺陷率、变更成本等度量出发,展示重构收益。
- 加分回答:可结合《重构》中“将过程式设计转化为对象设计”的手法,使用“提取方法对象”等重构技巧逐步迁移。
11.2 聚合过大的典型症状是什么?如何评估和拆分?
- 一句话回答:症状包括加载慢、乐观锁冲突高、SQL JOIN 过多;通过 SQL 日志和 jQAssistant 评估关联实体数,按写入足迹拆分。
- 详细解释:聚合过大导致事务边界过宽,并发冲突频繁,且未来微服务拆分困难。评估方法:开启 Hibernate SQL 日志观察一次聚合加载的 SQL 条数,或使用 jQAssistant 分析
@OneToMany集合数量。拆分策略采用“写入足迹法”:哪些对象在同一个业务用例中总是同时被修改,它们才属于同一聚合;其他情况拆为独立聚合,通过 ID 引用和领域事件保持最终一致。 - 多角度追问:
- 拆分后如何保证业务完整性?— 使用领域事件和最终一致性,必要时通过 Saga 补偿。
- 是否存在聚合过小的反模式?— 有时过度拆分会造成“聚合过小”,导致事务满天飞,需权衡。
- 如何处理拆分后查询需要 JOIN 的场景?— 引入 CQRS,读模型可随意 JOIN 而无需受聚合约束。
- 加分回答:Vaughn Vernon 在《实现领域驱动设计》中建议“设计小聚合”,并提供了具体的四个设计步骤。
11.3 资源库接口方法膨胀如何通过 Specification 模式解决?
- 一句话回答:膨胀源于按查询条件组合大量
findByXxx方法,可引入JpaSpecificationExecutor实现动态组合查询,将 Repository 方法收敛。 - 详细解释:Spring Data JPA 提供了
Specification接口,允许在业务层构建可组合的查询条件。将不同的查询条件定义成独立的Specification对象,通过where(…).and(…)组合,最终传入repository.findAll(spec),从而避免 Repository 接口爆炸。这与第 13 篇陈述式代码中的 Specification 模式一致,并可封装为 DSL 提高可读性。 - 多角度追问:
- Specification 是否存在性能隐患?— 可能产生复杂 SQL,需关注查询计划和索引。
- 与 QueryDSL 相比如何选择?— QueryDSL 更加类型安全,Specification 更轻量。
- 什么情况下仍需要专用查询方法?— 当查询有严格的性能要求且需要数据库特定优化时,可在 Repository 中直接使用
@Query。
- 加分回答:结合 CQRS,将复杂查询彻底转移到读模型(如 Elasticsearch),写模型 Repository 保持极简。
11.4 缺少防腐层会导致什么问题?ACL 的正确实现是怎样的?
- 一句话回答:外部模型变更会直接污染领域层,导致连锁修改;正确实现是在领域层定义 Port 接口,基础设施层通过 Adapter 和 Translator 隔离转换。
- 详细解释:防腐层(Anti-Corruption Layer)是 DDD 战略设计中的关键模式。当领域层直接依赖外部系统的 DTO 或 API 时,外部变化会迫使领域代码修改,破坏领域纯净性。正确做法:在领域层定义
PaymentPort接口,领域服务只依赖该接口;在基础设施层实现PaymentAdapter,负责调用外部 Feign 并进行模型翻译(Translator),将外部 DTO 转换为领域值对象。这样外部变更仅需修改适配器。 - 多角度追问:
- 多个外部系统如何设计 ACL?— 每个外部系统一个 Adapter,可按需实现多个 Port 的实例。
- 如何测试 ACL?— 使用 Spring Cloud Contract 的 Stub Runner 或 WireMock 模拟外部服务。
- ACL 是否会引入性能开销?— 模型转换开销微乎其微,主要成本在设计前期,但收益巨大。
- 加分回答:在微服务架构中,ACL 往往和 Gateway 模式结合,作为 BFF 的一部分。
11.5 如何检测跨界上下文直调?ArchUnit 规则如何配置?
一句话回答:跨界上下文直调表现为某个限界上下文的代码直接依赖另一个上下文的内部实现类(如 Repository 或 Service 实现),可通过 ArchUnit 包依赖规则检测并阻断。
详细解释:在模块化单体或微服务中,每个限界上下文应通过定义良好的端口(Port)暴露能力,消费者只依赖端口接口,不允许直接注入其他上下文的 Repository 或具体实现类。检测方法:使用 ArchUnit 的 layeredArchitecture 或自定义包依赖规则。例如,订单模块 com.ecommerce.order.. 应当只依赖 java..、org.springframework..、共享内核 com.ecommerce.shared..,以及本模块内部包,不得依赖 com.ecommerce.inventory.. 下的实现类。一旦发现违规,构建失败,强制通过端口/适配器方式重构。例如:
@ArchTest
public static final ArchRule order_should_not_depend_on_inventory_internal =
classes().that().resideInAPackage("..order..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage("..order..", "..shared..", "java..", "org.springframework..");
当 OrderService 中 import com.ecommerce.inventory.infrastructure.InventoryRepository; 时,ArchUnit 立即报错。
多角度追问:
- 如果两个上下文间确实需要共享数据,最佳实践是什么?—— 通过领域事件异步同步数据,或通过 Port 接口调用,绝对不共享数据库或 Repository。
- ArchUnit 包规则太严格,导致测试代码或配置类误报怎么办?—— 可使用
and().resideOutsideOfPackage("..test..")排除测试,或通过自定义条件放宽。 - 跨上下文直调的危害到底有多大?—— 它会破坏独立部署能力,当库存模块重构时,订单模块被迫修改,这与独立演进的微服务理念背道而驰。
加分回答:可以结合 jQAssistant 分析包依赖图,生成可视化报告,便于架构师快速定位违规连线。在《实现领域驱动设计》中,模块间通信只能通过接口,这种约束能有效防止“大泥球”架构。
11.6 领域事件的粒度如何判断过细或过粗?优化策略是什么?
一句话回答:事件粒度过细表现为每个属性变更都产生事件,过粗则用一个事件通知所有变更;判断标准是一个业务动作对应一个事件,且事件携带后续流程必需的最小数据集,优化策略是依据业务语义合并或分解事件。
详细解释:领域事件理想粒度是“一个业务事实一个事件”。如果订单的一次支付操作发布了 OrderPaid、OrderStatusChanged、OrderAmountUpdated 三个事件,就属于过细,导致消费者须处理大量事件才能拼凑出完整业务意图。反之,如果只使用一个 OrderUpdated 包含全部字段,消费者必须比对前后值才能判断是否支付,属于过粗,增加复杂度和耦合。排查时可统计:事件类型数量与聚合行为方法数量的比例(接近 1:1 为佳),以及平均事件字段数量(超过 20 往往过粗)。优化策略:与领域专家一起对业务流程建模,明确每个动作触发的业务事实,例如“订单已支付” OrderPaid,仅携带 orderId 和支付金额,消费者无需额外回查。
多角度追问:
- 事件过细导致的“事件风暴”会对消息中间件造成什么压力?—— 消息量激增,可能产生大量无意义的 I/O 和存储开销,增加中间件成本,并拖慢消费。
- 领域事件是否应该包含足够数据以避免消费者回查?—— 是的,这称为“事件增强”或“富事件”,应携带消费者所需数据,但不包含无关大对象,通常包含主键和必要的值对象。
- 如果不同消费者需要的数据不同,如何设计事件?—— 每个事件携带最小公共数据集,消费者可通过回调查询服务补充所需信息;或针对不同消费场景设计不同的事件类型,但须谨慎控制数量。
加分回答:可参考 Domain Event Design 的“Event Storming”产出,直接映射到代码,粒度自然准确。Vaughn Vernon 建议事件名称使用过去式动词短语,如 ProductPurchased,直观体现业务事实。
11.7 AI 如何辅助 DDD 代码审查?Prompt 如何设计?LLM 输出如何人工复核?
一句话回答:AI 通过定制 Prompt 扫描代码仓库,检测贫血模型、聚合过大、防腐层缺失等 DDD 反模式,输出结构化报告;人工需逐条验证真实性、排除误报、评估修正可行性后纳入债务看板。
详细解释:Prompt 设计需明确列出要检测的反模式清单及其判别规则,例如“检测 @Entity 类只有 getter/setter 而无业务方法”,并要求以 JSON 格式返回文件路径、描述和建议。LLM 能理解代码语义,比纯正则规则更灵活。复核三原则:① 真实性验证:打开文件检查 LLM 所指方法是否真的缺少业务逻辑;② 误报排除:某些类如值对象或遗留适配器可能被误判,人工标记;③ 修正可行性评估:LLM 建议可能是通用方案,需结合当前架构调整。通过后,将问题录入 Jira,标记 L1/L2/L3。AI 可集成到 CI 流水线中,对每个 PR 的 diff 进行增量扫描,大幅提升审查效率。
多角度追问:
- LLM 误报率高怎么办?—— 通过 few-shot 示例优化 Prompt,并提供反例;建立“白名单”类或包的过滤机制。
- AI 审查是否能完全替代人工架构审查?—— 不能,它只能作为“气味探测器”,架构的权衡和演进方向仍需专家判断。
- 如何保证 AI 扫描的代码隐私?—— 可使用私有化部署模型或仅提取必要代码片段上传,避免泄露核心业务逻辑。
加分回答:结合 Retrieval-Augmented Generation (RAG),可以注入团队内部的 DDD 设计规范文档,使 AI 审计更符合项目实际,并能生成培训材料。
11.8 陈述式代码退化为命令式的典型原因是什么?如何防止?
一句话回答:退化原因主要是团队成员不了解领域模型、开发压力下走捷径、缺少自动化约束;防止措施包括将聚合根 setter 私有化、编写 ArchUnit 规则禁止外部调用 setter、代码评审强化陈述式习惯。
详细解释:陈述式代码强调“做什么”而非“怎么做”,如 order.confirmPayment() 表达业务意图,而 order.setStatus(PAID) 则是命令式操作。退化常见于:新人接手后模仿旧代码,或快速修复 bug 时直接修改字段,绕过封装。一旦破窗,后续效仿,领域约束失效。根本解决方式:技术上,将所有状态修改的 setter 设为 protected 或 private,且只允许通过行为方法修改;在 CI 中加入 ArchUnit 规则检测应用层对 setXxx 的调用;流程上,将“聚合必须通过行为方法修改状态”写入编码规范,并在代码评审中作为强制条目。
多角度追问:
- JPA 要求有 setter,如何兼容?—— 可使用
@Access(AccessType.FIELD)让 JPA 直接通过字段访问,或将 setter 设为protected,JPA 仍可访问但业务代码无法调用。 - 是否所有状态修改都必须用行为方法?—— 对于纯粹的数据类(如 DTO),不要求,但聚合根的核心状态必须。
- 如何在一个大型遗留系统中逐步引入陈述式代码?—— 采用“绞杀者模式”,新功能使用行为方法,逐步将老代码中的
setStatus替换。
加分回答:引入状态机模式(如 Spring State Machine 或自定义 DSL),将状态转换显式化,强迫所有状态变更通过合法路径,可根治退化。
11.9 值对象与实体混淆的后果是什么?代码层面如何纠正?
一句话回答:混淆导致不必要的数据库行、相等性判断错误和额外的身份管理;纠正方式是将无业务标识的类改为 @Embeddable,实现不可变性和值比较相等。
详细解释:实体由其唯一标识区分,值对象则由其属性值定义。如果将一个表示地址的 Address 错误地注解为 @Entity 并赋予 @Id,每次用户修改地址时都会在表中产生新行,或更新时导致意外覆盖,无法基于值进行比较。正确的做法:将 Address 改为 @Embeddable,字段均为私有终态,提供全参构造器,不提供 setter,基于所有字段实现 equals 和 hashCode。JPA 会将其列嵌入到宿主表中,无需单独的 address 表。修正后,地址的相等性比较变得自然,且避免了孤儿数据。
多角度追问:
- 如果一个值对象需要被多个实体共享怎么办?—— 值对象可嵌入多个实体,各表都会持有其列,通常没必要共享。如果需要共享,说明它可能是一个实体。
- 如何处理值对象的数据库迁移?—— 将原
Address表列合并到宿主表,或保留表但移除@Entity,仅作为数据表由新值对象映射,这需要评估。 - 不可变值对象如何更新?—— 创建新实例替换旧字段,例如
person.setAddress(new Address(...)),这符合函数式编程思想,能避免副作用。
加分回答:在 DDD 中,尽量将许多概念建模为值对象,因为值对象无副作用、易测试、易理解。可参考《领域驱动设计》中关于“将值对象用于概念”的讨论。
11.10 事件未幂等会导致哪些数据一致性问题?分布式下如何处理?
一句话回答:非幂等事件消费导致重复扣款、重复发消息等数据错误;处理方式为在消费者侧引入去重表,利用数据库唯一约束或 Redis 原子操作实现仅处理一次。
详细解释:消息中间件可能因网络问题或消费者失败而重投事件。如果消费者没有去重逻辑,同一事件被处理两次会导致严重的业务偏差,例如余额重复扣除。解决方案:消费者在处理事件前,先检查事件 ID 是否已存在于 processed_events 表中,若存在则跳过;若不存在,则在同一个数据库事务中执行业务操作并插入事件 ID,利用唯一键约束防止并发重复。对于非事务性资源(如短信发送),可先插入去重记录,再发短信,若短信失败可通过补偿机制处理,保证最终一致性。
多角度追问:
- 去重表性能问题怎么解决?—— 可定期清理已处理事件,或对事件 ID 进行哈希分区。使用 Redis 的
SETNX也是一种轻量级选择,但需注意持久化和与数据库事务一致的问题。 - 如果事件携带的数据变了(如更新事件),还能用简单去重吗?—— 这种情况建议使用事件溯源或版本号,确保更新操作也是幂等的。
- 消息队列本身提供了去重机制吗?—— 例如 Kafka 的幂等生产者可以避免发送重复,但不能保证消费者端重复处理,所以消费端幂等依然必要。
加分回答:在企业级实现中,可使用“收件箱(Inbox)”模式,将事件接收和业务处理在同一个数据库事务中完成,利用关系型数据库的 ACID 特性保证仅处理一次。
11.11 CQRS 的适用边界是什么?过度设计有哪些信号?
一句话回答:CQRS 适用于读写模型差异大、读模型需要跨聚合复杂查询或高并发读的场景;过度设计的信号包括为简单 CRUD 引入事件同步、读模型与写模型几乎相同、增加无明显收益的复杂度。
详细解释:CQRS(命令查询职责分离)将写操作和读操作拆分为不同模型,通常搭配事件溯源或领域事件同步。其适用条件是:查询端需要连接多个聚合的数据、需要全文搜索或报表视图,或者读写负载差异极大需要独立扩展。如果只是简单的用户信息管理,写模型与读模型属性完全一致,引入 CQRS 反而增加了一个 ES 索引或只读副本,并且需要维护同步逻辑,增加延迟和维护成本。过度设计的典型信号:读模型仅由写模型通过几个 JOIN 就能实现;系统吞吐量很低;团队成员抱怨“为什么一个简单的查询要这么复杂”。此时应考虑回退到直接使用写模型 Repository 查询。
多角度追问:
- 如何平滑地从过度设计的 CQRS 回退?—— 渐进移除读模型同步事件,将查询重新指向写模型数据库,确保无影响后删除读模型存储。
- CQRS 是否一定要搭配事件溯源?—— 不是,CQRS 只是读写分离,可仅使用数据库主从复制实现读副本,无需事件。
- 在模块化单体中适合用 CQRS 吗?—— 适合,当需要跨上下文查询时,可构建一个查询服务订阅多个领域的领域事件,组装出视图,避免跨上下文直调。
加分回答:Greg Young 提出 CQRS 时强调它应该应用于特定的、有高价值的限界上下文,而不是整个系统。盲目 CQRS 是常见反模式。
11.12 未遵循小聚合的事务问题如何通过事件重构?
一句话回答:当一个事务修改多个聚合时,通过提取领域事件,将强一致性拆分为各聚合独立事务,实现最终一致性。
详细解释:典型场景是订单确认操作同时修改 Order、Payment、Inventory。重构方法:只让 Order 聚合在自己的事务内执行 order.confirm(),并发布 OrderConfirmed 事件;随后 Payment 和 Inventory 通过事件监听器在各自事务中处理,若某个步骤失败,可通过补偿事件(如 PaymentFailed)触发 Order.cancel()。这种基于事件的编排称为“事件驱动 Saga”。需注意幂等和重试。重构后,每个聚合的事务边界缩小,锁竞争降低,也为后续微服务拆分奠定基础。
多角度追问:
- 如果业务要求严格一致性(如支付与订单确认不能有偏差)怎么办?—— 可以将部分流程合并到同一个聚合内,或者采用 Saga 的补偿模式,确保最终一致,但会引入短暂的不一致窗口,需业务确认可接受。
- 如何处理事件丢失?—— 使用事务发件箱模式(outbox),将事件写入与业务操作在同一数据库事务中,然后由消息中继轮询发送,保证事件可靠投递。
- 重构时如何验证事件流程的正确性?—— 使用集成测试和“消费者驱动契约”测试,确保各个监听器行为符合预期。
加分回答:可引入轻量级工作流引擎(如 Camunda)来管理 Saga 的状态,使补偿逻辑更加清晰,避免回调地狱。
11.13 统一语言缺失如何度量?除了命名还有哪些地方需要对齐?
一句话回答:度量指标包括代码中非业务术语的出现频率、类/方法名与术语表不一致的数量;还需要在 API 文档、事件定义、数据库表和列名、日志和注释中全面对齐统一语言。
详细解释:统一语言不仅体现在类名(如 Order 而非 OrderInfo),还贯穿于以下地方:① 领域方法名:confirmPayment() 而非 updateStatus();② API 端点:/orders/{id}/payment-confirmation 而非 /updateOrderStatus;③ 数据库表:orders 表、buyer_id 列应反映术语;④ 事件:OrderPaid 而非 OrderStatusChanged;⑤ 日志和异常消息:应输出“买家已支付”而非“order status updated”。度量方法:通过静态分析工具扫描代码库,统计命名中出现的 Info、DTO、VO 等后缀,以及 CRUD 动词(create, update, delete)的使用频率。建立术语词典,用 ArchUnit 或 Checkstyle 禁止非规范命名。
多角度追问:
- 当领域专家和开发团队对某些术语存在分歧怎么办?—— 通过事件风暴达成共识,最终以领域专家的语言为准,因为统一语言来源于业务。
- 重构统一语言可能涉及数据库列重命名,如何安全实施?—— 可先添加新列,同步写入新旧列,然后逐步迁移读取,最后删除旧列,或使用视图过渡。
- 不同限界上下文中同一个业务概念可能有不同含义,如何处理?—— 在每个上下文内使用各自的语言,例如销售上下文的“订单”和物流上下文的“订单”可以有不同模型,只要在各自上下文内语言一致即可。
加分回答:Eric Evans 强调“代码就是模型,模型就是语言”,统一语言的缺失是 DDD 实施失败的首要原因。定期进行“语言一致性审查”是架构治理的必要环节。
11.14 (系统设计题)某电商系统经过一年 DDD 实践,季度评审发现了:订单模块贫血、支付模块缺 ACL、库存模块聚合过大乐观锁冲突 15%、通知模块事件未幂等。请设计完整治理方案,包括诊断、修复、ArchUnit 规则、AI 审查和优先级计划。
系统设计题详细解答:
1. 架构现状与问题域模型图:
flowchart LR
subgraph Contexts
order[订单上下文] -- 贫血模型 --> order
payment[支付上下文] -- 缺ACL --> payment
inventory[库存上下文] -- 聚合过大 --> inventory
notification[通知上下文] -- 事件未幂等 --> notification
end
order -- 调用 --> payment
order -- 调用 --> inventory
inventory -- 事件 --> notification
payment -- 事件 --> notification
2. 诊断过程(六步法):略,参照前文。 3. 修正方案与代码:
- 订单贫血:提取行为方法
order.pay()、order.cancel()。 - 支付 ACL:定义
PaymentPort,实现PaymentAdapter和PaymentTranslator。 - 库存拆分:
Warehouse、InventoryLog独立聚合,Inventory只保留StockItem;修改仓库地址不再竞争锁。 - 通知幂等:增加
processed_events表,消费者先检查eventId。
4. 防复发 ArchUnit 规则(5 条):
// 1. 贫血模型检测
// 2. 包依赖规则(订单不依赖库存 repo)
// 3. domain不依赖外部DTO
// 4. 应用层不调setter
// 5. Repository方法数限制
5. AI 辅助审查 Prompt 与流程: Prompt 如 7.2 节,流程:代码提交 -> AI 扫描 -> 输出 JSON -> 人工复核 -> Jira 创建。
6. 修复优先级与季度计划:
- L1(立即):贫血、ACL、聚合过大(3 天内启动修复)
- L2(下一迭代):幂等
- L3(观察):如无恶化顺延
时序图:订单支付流程修复后:
sequenceDiagram
participant App as AppService
participant Order as Order聚合
participant Port as PaymentPort
participant Adapter as Adapter
participant GW as 外部网关
App->>Order: pay(amount)
Order->>Order: 校验状态/金额
Order->>Order: status=PAID, 发布OrderPaidEvent
App-->>Port: 完成支付(可选异步)
Port->>Adapter: pay(amount)
Adapter->>GW: charge
GW-->>Adapter: response
Adapter-->>Port: PaymentConfirmation
治理完成后,系统架构健康度评分从 65 提升至 92 分。
附:反模式速查表 见前文第四章末尾综合表格。
延伸阅读:
- 《实现领域驱动设计》Vaughn Vernon
- 《Clean Code》Robert C. Martin
- 《Refactoring》Martin Fowler
- ArchUnit User Guide
- SonarQube Docs