You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DDD场景下如何将非领域对象数据传递至事件处理器

DDD场景下如何将非领域对象数据传递至事件处理器

嘿,我来帮你梳理下这个问题,其实你遇到的核心矛盾是觉得操作理由属于过程日志数据,不想污染领域模型,但又需要把它传递到事件处理器里生成日志。咱们一步步拆解解决方案:

首先要纠正一个小误区:操作理由虽然不影响聚合根的状态流转,但它是触发这些领域操作的必要业务上下文——你的业务规则明确要求执行提交/拒绝操作时必须提供理由,所以它完全可以作为领域方法的参数,并被包含在领域事件中,这并不违背DDD的原则,反而能在领域层保证业务约束的执行。

下面是具体的调整方案:

1. 调整聚合根方法,引入操作理由作为参数

把需要理由的领域方法(associate、reject)加上理由参数,同时在方法内部校验理由的合法性(比如非空),确保业务约束在领域层被遵守:

public class ApprovalFlow {
    // ... 原有字段不变

    public void associate(ApproverId approverId, String reason) {
        // 领域层校验理由非空,保证业务规则
        Objects.requireNonNull(reason, "提交理由不能为空");
        this.approverId = approverId;
        // 将理由传入领域事件
        eventBus.push(new AssociateEvent(approvalFlowId, approverId, reason));
    }

    public void reject(String rejectReason) {
        Objects.requireNonNull(rejectReason, "拒绝理由不能为空");
        this.status = Status.COMPLETED;
        this.approvalResult = ApprovalResult.REJECT;
        // 将拒绝理由传入领域事件
        eventBus.push(new RejectEvent(approvalFlowId, rejectReason));
    }

    // ... 原有枚举不变
}

2. 更新领域事件定义,包含理由数据

修改对应的领域事件,新增理由字段,让事件能携带操作时的上下文信息:

public class AssociateEvent {
    private final Long approvalFlowId;
    private final ApproverId approverId;
    private final String reason; // 新增理由字段

    public AssociateEvent(Long approvalFlowId, ApproverId approverId, String reason) {
        this.approvalFlowId = approvalFlowId;
        this.approverId = approverId;
        this.reason = reason;
    }

    // 对应的getter方法
    public String getReason() { return reason; }
}

public class RejectEvent {
    private final Long approvalFlowId;
    private final String rejectReason; // 新增拒绝理由字段

    public RejectEvent(Long approvalFlowId, String rejectReason) {
        this.approvalFlowId = approvalFlowId;
        this.rejectReason = rejectReason;
    }

    // 对应的getter方法
    public String getRejectReason() { return rejectReason; }
}

3. 调整上层调用链路,传递理由参数

把API层接收的理由参数一路传递到聚合根的方法中,确保上下文不丢失:

// remote api method - 提交给上级场景
public void submitNextApprover(Long approvalFlowId, Long nextApproverId, String reason) {
    check(approvalFlowId != null, "approvalFlowId cannot be null"); 
    check(nextApproverId != null, "nextApproverId cannot be null"); 
    check(StringUtils.isNotBlank(reason), "reason cannot be blank");

    commandService.submitNextApprover(approvalFlowId, nextApproverId, reason); // 新增reason参数
}

// command service - 提交给上级场景
public void submitNextApprover(Long approvalFlowId, Long nextApproverId, String reason) {
    ApprovalFlow approvalFlow = approvalRepository.find(approvalFlowId);
    approvalFlow.associate(new ApproverId(nextApproverId), reason); // 传递reason到聚合根
    approvalRepository.save(approvalFlow);
}

// remote api method - 拒绝场景
public void rejectApproval(Long approvalFlowId, String rejectReason) {
    check(approvalFlowId != null, "approvalFlowId cannot be null"); 
    check(StringUtils.isNotBlank(rejectReason), "rejectReason cannot be blank");

    commandService.rejectApproval(approvalFlowId, rejectReason); // 新增rejectReason参数
}

// command service - 拒绝场景
public void rejectApproval(Long approvalFlowId, String rejectReason) {
    ApprovalFlow approvalFlow = approvalRepository.find(approvalFlowId);
    approvalFlow.reject(rejectReason); // 传递rejectReason到聚合根
    approvalRepository.save(approvalFlow);
}

4. 事件处理器直接从事件中获取理由生成日志

现在事件里已经包含了所需的理由数据,处理器可以直接读取并生成日志:

// 提交给上级的事件处理器
@Subscribe
public void handleEvent(AssociateEvent associateEvent) {
    AssociateLogPo associateLog = new AssociateLogPo();
    associateLog.setApprovalFlowId(associateEvent.getApprovalFlowId());
    associateLog.setApproverId(associateEvent.getApproverId().getValue());
    associateLog.setReason(associateEvent.getReason()); // 直接从事件获取理由

    associateLogMapper.insert(associateLog);
}

// 拒绝操作的事件处理器
@Subscribe
public void handleEvent(RejectEvent rejectEvent) {
    RejectLogPo rejectLog = new RejectLogPo();
    rejectLog.setApprovalFlowId(rejectEvent.getApprovalFlowId());
    rejectLog.setRejectReason(rejectEvent.getRejectReason()); // 直接从事件获取拒绝理由

    rejectLogMapper.insert(rejectLog);
}

为什么这个方案符合DDD?

  • 没有污染聚合根的持久化状态:理由只是作为操作的临时上下文传入,不会被持久化到ApprovalFlow实体中,保持了领域模型的纯净性。
  • 业务约束在领域层落地:把理由校验放在聚合根方法里,避免了上层代码漏传理由的情况,确保业务规则被严格执行。
  • 领域事件完整记录操作上下文:领域事件的核心作用就是记录领域操作发生时的关键信息,把理由包含进去,让事件能完整描述操作的全貌,这完全符合事件驱动的设计思路。

对比你之前的尝试:

  • 第一种“找日志再填充”的方法,存在操作和日志无法精准匹配的风险(比如短时间内多个同类型操作),可靠性差;
  • 第二种“临时存储再查询”的方法,绕过了领域模型,会导致领域逻辑和外部存储耦合,后续维护成本很高。

这个方案既解决了数据传递的问题,又严格遵循了DDD的设计原则,应该能完美适配你的业务场景。

备注:内容来源于stack exchange,提问作者Jeremy Huang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 10:43:03