
【架构实战】领域驱动设计DDD复杂业务系统的建模与落地一、背景一个订单状态引发的血案2019年底我们接了一个保险理赔系统。需求文档300页理赔流程涉及报案、查勘、定损、核赔、支付、结案六个环节每个环节又有十几种状态和分支条件。团队按传统三层架构Controller-Service-DAO开始写代码。三个月后代码仓库里出现了一个8000行的ClaimService.java5000行的PolicyService.java以及满天飞的if-else。某天测试提了一个Bug“拒赔后又赔了”。排查了一天发现是某个状态机逻辑被绕过去了——因为业务规则散落在五个Service里没有人能说清楚完整的状态流转。这就是贫血模型的典型症状业务逻辑散落在各个Service中缺乏统一表达数据模型Entity只有getter/setter沦为纯粹的数据容器开发与业务沟通成本高代码读不懂业务意图改一个需求动辄影响十几个类CTO拍板重构上DDD。二、DDD核心概念从贫血到充血2.1 什么是DDDDDDDomain-Driven Design领域驱动设计由Eric Evans在2004年提出核心理念是将业务领域的核心逻辑封装在领域模型中代码结构与业务结构保持一致。关键转变【贫血模型】 Entity只有 getter/setter ↓ Service承载所有业务逻辑,越来越胖 ↓ 数据库数据存储 【充血模型DDD】 聚合根Aggregate Root封装业务规则和行为 ↓ 实体Entity有唯一标识和业务行为 ↓ 值对象Value Object无标识不可变 ↓ 领域服务Domain Service处理跨聚合的业务逻辑2.2 DDD战略设计三件套概念定义例子限界上下文一个业务边界内部模型一致、语义统一理赔上下文、承保上下文、支付上下文通用语言团队共享的业务术语代码、文档、对话统一“保单已出单而不是状态字段status3”上下文映射上下文之间的协作关系理赔上下文通过防腐层调用支付上下文实战体会DDD的DDesign其实不太重要真正重要的是前面两个DDomain-Driven。通用语言是DDD落地的第一关——业务与开发用同一种语言对话需求不会在翻译中丢失。三、落地方案从战略设计到战术实现3.1 限界上下文拆分我们按业务能力拆分了五个限界上下文┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 投保上下文 │ │ 承保上下文 │ │ 理赔上下文 │ │ (Policy) │──▶│(Underwrite) │──▶│ (Claim) │ └─────────────┘ └─────────────┘ └──────┬──────┘ │ ┌─────────────┼─────────────┐ │ │ │ ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ │ 支付上下文 │ │ 风控上下文 │ │ 通知上下文 │ │(Payment) │ │(RiskCtrl) │ │(Notify) │ └───────────┘ └───────────┘ └───────────┘每个上下文对应一个独立的微服务有自己的数据库。3.2 聚合设计理赔上下文为例// 聚合根理赔单EntitypublicclassClaimimplementsAggregateRoot{privateClaimIdid;// 值对象理赔单IDprivatePolicyIdpolicyId;// 值对象关联保单IDprivateClaimAmountamount;// 值对象理赔金额privateClaimStatusstatus;// 值对象理赔状态privateListClaimDocumentdocuments;// 实体理赔材料privateListClaimHistoryhistories;// 实体操作历史privateAuditorauditor;// 值对象审核人// 业务行为核心 /** 提交理赔 */publicvoidsubmit(ListDocumentdocs){if(this.status!ClaimStatus.DRAFT){thrownewDomainException(只有草稿状态的理赔单才能提交);}if(docs.isEmpty()){thrownewDomainException(理赔材料不能为空);}this.statusClaimStatus.SUBMITTED;this.documentsdocs.stream().map(ClaimDocument::from).collect(Collectors.toList());this.addHistory(提交理赔申请);}/** 定损 */publicvoidassess(DamageAssessmentassessment){if(this.status!ClaimStatus.SUBMITTED){thrownewDomainException(只有已提交的理赔单才能定损);}this.amountassessment.getEstimatedAmount();this.statusClaimStatus.ASSESSED;this.addHistory(定损完成预估金额assessment.getEstimatedAmount());}/** 核赔通过 */publicvoidapprove(){if(this.status!ClaimStatus.ASSESSED){thrownewDomainException(只有已定损的理赔单才能核赔);}this.statusClaimStatus.APPROVED;this.addHistory(核赔通过);}/** 拒赔 */publicvoidreject(Stringreason){if(this.statusClaimStatus.PAID||this.statusClaimStatus.CLOSED){thrownewDomainException(已完成或已关闭的理赔单不能拒赔);}this.statusClaimStatus.REJECTED;this.addHistory(拒赔原因reason);}privatevoidaddHistory(Stringdescription){this.histories.add(newClaimHistory(description,LocalDateTime.now()));}}// 值对象理赔金额EmbeddablepublicclassClaimAmount{privateBigDecimalamount;privateCurrencycurrency;publicClaimAmountadd(ClaimAmountother){if(!this.currency.equals(other.currency)){thrownewDomainException(币种不一致无法相加);}returnnewClaimAmount(this.amount.add(other.amount),this.currency);}// 值对象不提供setter不可变}代码对比维度贫血模型充血模型DDD业务逻辑在哪ClaimService8000行Claim聚合根各方法职责清晰状态校验散落在Service各处聚合根内部统一入口测试只能测Service可在单元测试层测业务规则可读性需要理解全部Service逻辑聚合根就是业务文档3.3 领域服务与领域事件// 领域服务处理跨聚合的理赔支付ServicepublicclassClaimPaymentService{AutowiredprivateClaimRepositoryclaimRepository;AutowiredprivatePaymentGatewaypaymentGateway;// 防腐层接口AutowiredprivateApplicationEventPublishereventPublisher;/** 支付理赔款 */TransactionalpublicvoidpayClaim(ClaimIdclaimId){// 1. 加载聚合ClaimclaimclaimRepository.findById(claimId).orElseThrow(()-newDomainException(理赔单不存在));// 2. 调用支付网关通过防腐层PaymentResultresultpaymentGateway.pay(newPaymentRequest(claim.getAmount(),claim.getPayee()));// 3. 更新聚合状态claim.markAsPaid(result.getTransactionId());claimRepository.save(claim);// 4. 发布领域事件eventPublisher.publishEvent(newClaimPaidEvent(claim.getId(),claim.getAmount()));}}// 领域事件GetterpublicclassClaimPaidEventextendsDomainEvent{privatefinalClaimIdclaimId;privatefinalClaimAmountpaidAmount;publicClaimPaidEvent(ClaimIdclaimId,ClaimAmountpaidAmount){super(LocalDateTime.now());this.claimIdclaimId;this.paidAmountpaidAmount;}}3.4 防腐层Anti-Corruption Layer用于隔离外部系统对领域模型的污染// 防腐层接口领域层定义publicinterfacePaymentGateway{PaymentResultpay(PaymentRequestrequest);}// 防腐层实现基础设施层ComponentpublicclassWechatPaymentGatewayimplementsPaymentGateway{AutowiredprivateWechatPayClientwechatPayClient;OverridepublicPaymentResultpay(PaymentRequestrequest){// 将领域对象转换为微信支付的DTOUnifiedOrderRequestwxReqWechatPayConverter.toWechatRequest(request);// 调用外部APIUnifiedOrderResponsewxRespwechatPayClient.unifiedOrder(wxReq);// 将外部返回转换为领域对象returnWechatPayConverter.toPaymentResult(wxResp);}}四、仓库模式与持久化分离// 仓库接口领域层publicinterfaceClaimRepository{OptionalClaimfindById(ClaimIdid);ListClaimfindByPolicyId(PolicyIdpolicyId);voidsave(Claimclaim);voiddelete(ClaimIdid);}// 仓库实现基础设施层基于JPARepositorypublicclassJpaClaimRepositoryimplementsClaimRepository{AutowiredprivateJpaClaimDaoclaimDao;AutowiredprivateJpaClaimDocumentDaodocumentDao;OverrideTransactionalpublicvoidsave(Claimclaim){// 聚合根子实体一起持久化ClaimPOclaimPOClaimConverter.toPO(claim);claimDao.save(claimPO);// 级联保存理赔材料ListClaimDocumentPOdocPOsclaim.getDocuments().stream().map(doc-ClaimConverter.toDocumentPO(doc,claim.getId())).collect(Collectors.toList());documentDao.saveAll(docPOs);}}一个聚合一个仓库一次事务只修改一个聚合——这是DDD的铁律。五、DDD落地的四大坑5.1 坑一过度设计症状项目启动第一周就在画限界上下文、设计聚合、定义值对象代码没写几行。解决先写代码后建模。迭代式DDD第一版按照直觉拆分模块第二版调整聚合边界第三版引入领域事件。不要追求完美的DDD先跑起来。5.2 坑二聚合边界不清症状聚合太大一次加载几千条数据聚合太小到处是跨聚合的事务问题。解决小聚合原则——一个事务只修改一个聚合。如果发现一个业务操作需要同时修改两个聚合检查是否应该把它们合并或者通过领域事件解耦。5.3 坑三通用语言推行不下去症状文档里写理赔单已核赔通过代码里写的claim.setStatus(5)讨论的时候说状态改成5了。解决用代码约束语言。用枚举代替魔法数字用值对象包装原始类型让代码本身就是通用语言的载体。业务人员看不懂代码但能看懂方法名claim.approve()。5.4 坑四DDD面试造火箭工作拧螺丝症状花重金招了DDD架构师实际业务就两张表CRUD。解决不是所有业务都适合DDD。DDD的核心价值是应对复杂业务逻辑。如果业务主要是CRUD用DDD是杀鸡用牛刀。判断标准业务流程的if-else超过三层吗状态机超过10个状态吗业务规则频繁变化吗如果三个回答都是否别用DDD。六、总结DDD不是银弹它是一套应对复杂性的建模方法论。核心收获通用语言是第一步团队先统一术语DDD才有落地基础聚合是战术核心设计好的聚合DDD成功了一半。遵循小聚合、一次事务一个聚合的原则充血模型让代码会说话业务规则写在聚合根的方法里代码即文档防腐层是架构安全阀不要让外部系统的数据格式污染你的领域模型迭代优于一次到位DDD建模是动态过程不要追求一步到位选型建议业务类型推荐架构简单CRUD三层架构Controller-Service-DAO中等复杂度10-50个状态DDD Lite只用聚合值对象高复杂度50状态、多分支完整DDD CQRS Event Sourcing领域驱动设计的本质不是技术而是让代码反映业务。当你读代码就能理解业务流程的时候DDD就落地了。个人观点仅供参考