我最近一直在深入研究领域驱动设计(DDD),并尝试将其应用到工作项目中。然而,我遇到了一些障碍,需要你的建议;
我们有审批系统。员工可以审核客户的申请并决定是否批准。由于员工技能水平存在差异,初级员工在无法确定是否批准申请时,可以将审批转给高级员工。 因此我们设计了一个工作流程系统。
ApprovalFlow
是AggregateRoot,用于处理员工的所有审批操作。 Status
是aggregateRoot的状态。 ApproverId
是审核工作流程的员工ID。其字段如下:
ApprovalFlow {
private EventBus eventBus;
private Long approvalFlowId;
private Status status;
private ApprovalResult approvalResult;
private ApproverId approverId;
public void associate(ApproverId approverId) {
this.approverId = approverId;
eventBus.push(new AssociateEvent(approvalFlowId, approverId));
}
public void approve() {
this.status = Status.COMPLETED;
this.approvalResult = ApprovalResult.PASS;
eventBus.push(new ApproveEvent(approvalFlowId))
}
public void reject() {
this.status = Status.COMPLETED;
this.approvalResult = ApprovalResult.REJECT;
eventBus.push(new RejectEvent(approvalFlowId));
}
enum Status {
APPROVING(1),
COMPLETED(2);
private Integer code;
Status(Integer code) {
this.code = code;
}
}
enum ApprovalResult {
PASS(1),
REJECT(2);
private Integer code;
Status(Integer code) {
this.code = code;
}
}
}
审批率和审批效率是业务部门关注的指标。为了量化这些指标,我们要求员工在系统上进行审批操作时(如批准/拒绝客户申请、提交高级员工审批等)提供相关理由。系统会存储员工的审批操作及理由。
现在,当员工决定向高级员工提交申请或拒绝申请时,必须提供相关操作理由。只有批准操作,而不是操作原因影响工作流的状态流程。我们倾向于查看 操作原因作为流程数据,例如操作日志,而不是业务逻辑。因此,我们没有将操作原因建模为领域对象。
我们目前面临以下两个业务场景的困难:
// submit customer's application to senior employee scenario
// remote api method
public void submitNextApprover(Long approverFlowId, 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(approverFlowId, nextApproverId);
}
// command service
public void submitNextApprover(Long approvalFlowId, Long nextApproverId) {
ApprovalFlow approvalFlow = approvalRepository.find(approvalFlowId);
approvalFlow.associate(new ApproverId(nextApproverId));
approvalRepository.save(approvalFlow);
}
// AggregateRoot associate method
public void associate(ApproverId approverId) {
this.approverId = approverId;
eventBus.push(new AssociateEvent(approvalFlowId, approverId));
}
// domain event handler and write a submitNextApprover log;
@Subscribe
public void handleEvent(AssociateEvent associateEvent) {
AssociateLogPo associateLog = new AssociateLogPo();
associateLog.setApprovalFlowId(associateEvent.getApprovalFlowId);
associateLog.setApproverId(associateEvent.getApproverId);
// can't transfer reason through associateEvent to event handler,
// because the reason is not a domain object
associateLog.setReason(reason);
associateLogMapper.insert(associateLog);
}
// reject customer's application scenario
// 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(Long approvalFlowId);
}
// command service
public void rejectApproval(Long approvalFlowId) {
ApprovalFlow approvalFlow = approvalRepository.find(approvalFlowId);
approvalFlow.reject();
approvalRepository.save(approvalFlow);
}
// AggregateRoot reject method
public void reject() {
this.status = Status.COMPLETED;
this.approvalResult = ApprovalResult.REJECT;
eventBus.push(new RejectEvent(approvalFlowId));
}
// domain event handler and write a reject approval log;
@Subscribe
public void handleEvent(RejectEvent rejectEvent) {
RejectLogPo rejectLog = new RejectLogPo();
rejectLog.setApprovalFlowId(rejectEvent.getApprovalFlowId);
// can't transfer rejectReason through rejectEvent to event handler,
// because the rejectReason is not a domain object
rejectLog.setRejectReason(rejectReason);
rejectLogMapper.insert(rejectLog);
}
之前的尝试: 我们之前提出了两个解决方案:
有人可以帮我想出解决办法吗?谢谢!
听起来好像您认为某个操作的原因属于与workflow子域不同的子域。您可能有充分的理由这样做,但这确实使事情变得更加复杂。设计选择是否值得额外的复杂性总是值得考虑的。然而,为了便于论证,让我们假设情况确实如此。
此类问题的典型解决方案是使用关联 ID。在异步、基于消息的系统中,这通常是一个好主意。确保每个实体都有唯一的 ID,最好在它第一次存在时为其分配一个 UUID。将此 ID 与与该实体相关的所有消息一起使用。因此,当工作流被拒绝时,系统可能会发布多条消息,在这种情况下:
reject
)workflowRejected
)尽管不同,但每条消息都具有相同的关联 ID。如果您稍后需要将应用程序及其当前状态给出的原因收集在一起,这使您能够实现聚合器。