当前位置: 首页 > news >正文

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 获取商品信息
交易 → 库存交易通过防腐层调用库存能力,只保留“锁定库存”等本地业务语义
支付 → 交易支付成功后发布消息,交易根据契约更新订单支付结果
交易 → 会员订单完成后通知会员增加成长值,双方通过公开事件格式协作

把这些关系画成上下文映射图,大概就是下面这样:

图中的实线表示同步调用,虚线表示异步事件。交易上下文调用库存能力时增加了一层模型转换,避免库存上下文的字段和协议直接进入交易模型。

这里需要注意,上下文映射表达的是业务边界之间的关系,不等于微服务调用拓扑图。

一个限界上下文可以暂时和另一个限界上下文部署在同一个应用里,它们仍然是两个模型边界。反过来,一个微服务如果塞进了多个互相混乱的业务模型,也不能因为已经拆成服务,就认为边界一定合理。

我们可以用下面几个问题检查映射是否清楚:

  1. 调用关系的上下游是否明确;
  2. 对外契约由谁维护;
  3. 上游字段变化会不会直接冲击下游领域模型;
  4. 外部异常在哪里转换;
  5. 双方使用的同名概念是否真的含义一致;
  6. 这段共享或依赖是否真的有必要。

如果这些问题回答不出来,说明边界虽然画了,但上下文之间的关系还没有梳理清楚。

这些模式是描述真实关系的工具,不是必须全部套用的设计检查表。

七、总结

这一篇我们继续学习了 DDD 的战略设计,主要整理了通用语言和上下文映射。

简单总结一下:

  • 通用语言用于统一同一个限界上下文内部的业务表达;
  • 通用语言要进入需求、模型、接口和代码,不能只停留在会议上;
  • 同一个词在不同限界上下文中可以有不同含义;
  • 上下文映射用于描述不同限界上下文之间的依赖和协作关系;
  • 防腐层可以隔离外部模型,公开服务契约可以稳定上下文之间的协作;
  • 上下文映射不等于微服务调用图,更不能代替真实业务边界分析。

到这里,我们已经知道了怎么划分边界、怎么统一边界内的语言,以及边界之间怎么协作。

不过这些结果不能只靠几个人坐在会议室里面拍脑袋决定。下一篇文章,我们继续学习事件风暴,看看怎么让业务、产品、开发和测试一起把业务流程、关键事件和边界梳理出来。

http://www.jsqmd.com/news/1179315/

相关文章:

  • Python agent-flows 包:功能详解、安装配置与实战案例
  • 工业负载控制方案:TPD2015FN与STM32F446ZE应用实践
  • 信息学奥赛一本通 1162 题解:从字符串逆序到递归思想的 3 个进阶应用
  • 2026 年现阶段,旌德值得关注的聚氨酯组合料定做厂家全面解析与选购指南,这材料竟让建筑成本暴跌一半,老板看完沉默了30分钟? - 行业鉴选官
  • 工业负载控制方案:TPD2015FN与MCU的可靠设计
  • Godot Tween动画开发指南:从基础原理到工程化实践
  • STM32与TB6593FNG的直流电机控制方案设计与优化
  • UE5 Level Streaming实战:从机制到避坑,解决加载卡顿与物体弹出
  • 遗传算法实战:100皇后问题的参数调优与调试避坑指南
  • Pydantic+LangChain构建健壮AI后端:校验与编排双驱动
  • STM32控制TPS61170实现高效升压转换方案
  • 阻抗控制 DOB 扰动观测器:无需力传感器的 2 种末端接触力估计方法
  • 基于MA12070与STM32F415ZG的高保真音频系统设计
  • UE Niagara粒子系统Bug排查:导弹追踪效果稳定性优化实战
  • Unity VR 360°视频播放性能优化:从卡顿到流畅72fps的完整方案
  • Windows 8 Build 7777预发布版实测:界面优化与性能改进分析
  • TMC7300与PIC18F67K40组合优化有刷直流电机控制
  • Minecraft模组开发全流程:从Java基础到自定义功能实现
  • 工业负载控制:TPD2015FN高侧开关芯片应用解析
  • FTP 21端口安全配置实战:vsftpd 3.0.5 部署与5项关键加固策略
  • IPS 与 CATIA/NX 数据桥接配置:5 步实现三维模型双向同步更新
  • DC-DC升压转换器设计与PIC微控制器集成方案
  • 梯形 vs S型速度曲线:5维性能对比与工业机器人选型指南
  • Windows系统免费重装指南:官方工具使用与数据备份全流程
  • Godot 4 UI布局容器实战:从锚点原理到复杂界面搭建
  • BepInEx插件框架入门:Unity游戏Mod开发与安装实战指南
  • STM32F756ZG与ADS1262高精度数据采集系统设计
  • Windows系统自带免费重装功能详解:重置此电脑操作指南
  • 2026年7月最新北京爱彼官方售后客户服务热线与维修网点地址汇总 - 爱彼中国官方服务中心
  • 高效使用Genshin FPS Unlocker的完整指南:突破原神60帧限制的实用方案