DDD 系列:通用语言和上下文映射
上一篇文章中我们学习了领域、子域和限界上下文知道了 DDD 的战略设计首先要解决一个问题哪些业务应该放在一起哪些业务应该分开。不过边界划分出来只是第一步。真实项目中即使大家都在讨论“订单”产品、开发、测试和财务理解的也可能不是同一个东西。如果这些词没有统一模型、接口和代码就容易各说各话边界之间没有明确关系服务调用也会慢慢变成互相依赖。所以这一篇我们继续学习战略设计里面的两个概念通用语言Ubiquitous Language解决同一个上下文内部如何统一表达上下文映射Context Map解决不同限界上下文之间如何协作。一、为什么业务和代码总是对不上产品说“用户提交订单后可以取消订单。”这句话听起来没有问题但开发真正开始写代码时一堆问题就出来了“提交订单”是保存草稿、创建成功还是已经支付用户取消和客服关闭是不是同一个动作取消后需不需要释放库存、退回优惠券已经支付的订单到底叫取消还是退款。如果这些问题没有说清楚代码里面很可能出现下面这种方法updateOrderStatus(orderId,4);4是什么意思为什么能改成4修改之后还要做什么只看这行代码完全不知道。另一个开发可能又写了一个closeOrder(orderId);这时候“取消订单”和“关闭订单”到底是不是一回事也没人能说清楚。问题的根源不是方法名没起好而是团队没有先统一业务语言。二、什么是通用语言通用语言英文叫 Ubiquitous Language也有人翻译成统一语言。通用语言是一套由业务人员和开发人员共同建立并且在同一个限界上下文中持续使用的业务语言。也就是说团队在讨论业务、写需求、画模型、定义接口和编写代码时应使用同一套含义明确的词。1. 不是开发自己定义一套术语通用语言不是开发人员关起门来给类和字段起名字。它应该来自业务讨论由产品、业务、开发和测试共同确认。比如业务上明确区分用户主动放弃未支付订单叫作取消订单超时未支付由系统终止叫作关闭订单已支付订单退回资金叫作退款。那么代码里面也应该保留这种区别order.cancel();order.close();order.refund();而不是全部写成order.updateStatus(status);前一种写法能直接表达业务动作后一种写法只是修改数据。2. 通用语言只在特定上下文中成立上一篇文章中我们提到限界上下文会给模型划定生效范围。通用语言同样受这个边界约束。同一个词在不同限界上下文中可以有不同含义。还是以电商系统为例限界上下文“商品”的含义重点关注的信息商品上下文可维护和展示的商品资料名称、类目、品牌、详情交易上下文用户下单时购买的交易项成交价、购买数量、优惠快照库存上下文可以入库、出库和锁定的库存单元SKU、可用量、锁定量、仓库它们都可能说“商品”但是关注的不是同一个模型。如果为了追求全系统统一强行让三个上下文共用一个Product类这个类最后就会同时包含展示、交易、库存等字段改任何一处都可能影响其他模块。所以通用语言追求的是上下文内部一致不是整个系统只有一套万能模型。这张图强调的不是三个对象应该怎么设计而是语言和模型都有明确的生效边界。三、通用语言怎么落到代码里通用语言不能只停留在会议纪要或者术语表里它最终要进入代码。如果开会时说“取消订单”代码中却叫changeStatus团队很快还会回到各说各话的状态。1. 类名要体现业务角色不要只根据数据库表名机械生成对象。比如交易上下文中的订单可以使用Order支付上下文里记录支付过程的对象可以使用PaymentOrder。即使它们在数据库里都带有order字样也不代表它们是同一个业务对象。2. 方法名要体现业务动作业务代码里面优先使用有业务含义的方法order.submit();order.cancel();order.confirmPayment();少用看不出意图的名字order.update();order.handle();order.process();一个好的业务方法名应该让我们不看方法实现也能大概知道它在做什么。3. 状态和规则也要使用业务词汇下面这种状态虽然能运行但是阅读成本很高privateIntegerstatus;业务允许的情况下可以把状态表达得更明确publicenumOrderStatus{WAITING_FOR_PAYMENT,PAID,CANCELED,CLOSED}需要注意这里不是说所有字段都必须改成枚举也不是说用了枚举就是 DDD。重点是代码中的概念和业务讨论中的概念要对应得上。4. 发现歧义时及时更新语言通用语言不是项目启动时整理一次以后就不变了。随着业务深入我们可能会发现原来的词不够准确。比如最开始只有“退款”后来业务增加了仅退款、退货退款、部分退款这时候模型和代码也要跟着调整。如果一个词在会议中需要反复解释或者不同人总是给出不同理解通常就说明这个词还没有定义清楚。四、什么是上下文映射限界上下文划分完成后各个上下文并不会完全隔离。下单需要查询商品下单成功需要锁定库存支付成功需要通知订单订单完成以后可能还要增加会员积分。这些协作关系放在一起就是上下文映射。上下文映射是对多个限界上下文之间关系、依赖方向和协作方式的明确描述。它关注的不是某个类怎么写而是下面这些问题哪个上下文依赖哪个上下文谁是上游谁是下游双方是否共享模型外部模型进入当前上下文时要不要转换上游发生变化时下游会受到多大影响。通俗一点讲限界上下文告诉我们“边界画在哪里”上下文映射告诉我们“边界之间怎么打交道”。五、常见的上下文映射关系上下文映射有不少模式。这里先介绍项目中比较容易遇到的几种重点理解它们解决什么问题不需要一上来背所有英文缩写。1. 客户方—供应方客户方—供应方也叫 Customer/Supplier。上游上下文提供能力下游上下文使用能力。下游对接口有明确诉求并且可以参与上游接口或发布计划的协商。比如商品上下文向交易上下文提供商品查询能力。交易上下文是使用方它可能要求接口稳定返回商品是否可售、当前价格等信息。这种关系不是简单的“我调你接口”重点在于下游需求能够进入上游的协作计划。2. 防腐层防腐层也叫 Anticorruption Layer简称 ACL。当外部系统的模型和当前上下文差异较大时我们不直接把外部对象带进领域模型而是在边界处做一次转换。例如外部库存系统返回{sku_id:SKU-1001,available_qty:20,locked_qty:3}当前交易上下文真正关心的可能只是“本次购买数量是否可用”。这时候可以通过防腐层转换成当前上下文自己的表达publicinterfaceInventoryGateway{booleanisAvailable(StringskuId,intquantity);}外部接口字段、协议和异常处理都留在适配实现中领域代码只依赖自己能理解的能力。**防腐层的核心价值是保护当前上下文的模型不被外部模型污染。**至于防腐层具体怎么写后面的进阶章节会单独展开这里先理解它在上下文关系中的位置。3. 开放主机服务和发布语言开放主机服务叫 Open Host Service发布语言叫 Published Language。当一个上下文需要向多个调用方稳定提供能力时可以设计一套公开、稳定的服务协议并使用双方都能理解的数据格式进行交互。常见形式包括 REST API、RPC 接口、消息事件、OpenAPI 文档和明确版本的 DTO。这里的关键不是用了 HTTP 还是 MQ而是对外契约要稳定并且不能直接暴露上下文内部模型。4. 其他常见关系关系适用场景注意事项合作关系两个上下文共同开发一项业务一方修改契约时要同步评估另一方共享内核双方共享少量含义完全一致的模型或规则共享范围必须小并由双方共同维护遵奉者下游无法要求上游调整只能接受其模型和接口接入简单但下游容易受上游模型影响各行其道集成成本大于收益各自实现更简单不是所有重复都值得抽成共享能力六、用一个电商场景串起来假设现在有商品、交易、库存、支付和会员五个限界上下文。它们之间可能存在下面这些关系上下文关系协作方式商品 → 交易商品提供公开查询接口交易使用稳定 DTO 获取商品信息交易 → 库存交易通过防腐层调用库存能力只保留“锁定库存”等本地业务语义支付 → 交易支付成功后发布消息交易根据契约更新订单支付结果交易 → 会员订单完成后通知会员增加成长值双方通过公开事件格式协作把这些关系画成上下文映射图大概就是下面这样图中的实线表示同步调用虚线表示异步事件。交易上下文调用库存能力时增加了一层模型转换避免库存上下文的字段和协议直接进入交易模型。这里需要注意上下文映射表达的是业务边界之间的关系不等于微服务调用拓扑图。一个限界上下文可以暂时和另一个限界上下文部署在同一个应用里它们仍然是两个模型边界。反过来一个微服务如果塞进了多个互相混乱的业务模型也不能因为已经拆成服务就认为边界一定合理。我们可以用下面几个问题检查映射是否清楚调用关系的上下游是否明确对外契约由谁维护上游字段变化会不会直接冲击下游领域模型外部异常在哪里转换双方使用的同名概念是否真的含义一致这段共享或依赖是否真的有必要。如果这些问题回答不出来说明边界虽然画了但上下文之间的关系还没有梳理清楚。这些模式是描述真实关系的工具不是必须全部套用的设计检查表。七、总结这一篇我们继续学习了 DDD 的战略设计主要整理了通用语言和上下文映射。简单总结一下通用语言用于统一同一个限界上下文内部的业务表达通用语言要进入需求、模型、接口和代码不能只停留在会议上同一个词在不同限界上下文中可以有不同含义上下文映射用于描述不同限界上下文之间的依赖和协作关系防腐层可以隔离外部模型公开服务契约可以稳定上下文之间的协作上下文映射不等于微服务调用图更不能代替真实业务边界分析。到这里我们已经知道了怎么划分边界、怎么统一边界内的语言以及边界之间怎么协作。不过这些结果不能只靠几个人坐在会议室里面拍脑袋决定。下一篇文章我们继续学习事件风暴看看怎么让业务、产品、开发和测试一起把业务流程、关键事件和边界梳理出来。