一文说清:DDD对产品的实际用处

0 评论 608 浏览 1 收藏 13 分钟

DDD(领域驱动设计)正成为产品经理解决复杂业务系统映射难题的利器。从统一业务概念到划分系统边界,从实体识别到聚合设计,本文通过支付、物流等实际案例,深入解析如何用DDD思维构建高内聚、低耦合的产品架构,实现业务与系统的精准匹配。

DDD对产品的实际用处

建立复杂业务与产品系统的“映射”桥梁,从业务领域上拆解、建立并明确系统模块及其模块边界,规划系统架构,定义系统关键字字段等。

1、建立统一共识

核心名称统一含义。产品与开发围绕业务模型进行沟通,避免歧义或增加沟通成本。

例如,在“支付”模块,关键字有“卖家”“买家”“交易单”;在“物流”模块,关键字有“收货人”“发货单”。

从业务上,支付模块有关键字“买家”,物流模块有关键字“收件人”,买家可能等于收货人。

2、业务关键字映射系统,明确系统信息边界

从业务出发,建构系统模块,划分“业务逻辑映射成为功能模块”的系统/模块边界,明确业务名称在系统不同模块内的字段。

例如,从“必须先有商品”的业务模式看,商品→库存→订单,若虚拟商品不存在库存的情况,那么“商品→订单”。系统内的“商品”,与业务上称的“商品”内涵不一致。系统中的商品,在不同模块存储不同字段值,来记录或扩展商品信息;业务中的商品,单指商品本身。

同一个“商品”在业务的生命周期中,会进入不同的限界上下文,每个上下文只关心它的一部分特征。

业务关键字“商品”。在“订单”里,用户购买的哪件商品,数据表需要的“商品”信息有 SKU ID、名称快照、价格快照;在“库存”里,卖家所提供商品的库存信息,数据表需要的“商品”信息有 SKU ID、物理库存。

3、实体V值对象

判断哪些数据需要跟踪生命周期,哪些数据仅临时承载信息,这将影响数据设计和接口定义。

例如,用户管理系统中,“用户”有唯一 ID,改了名字还是同一个人,这是实体。“收货地址”,如果只是由“省市区+街道”组成,没有独立 ID,换一个地址就是不同的东西,这是值对象;有 ID的收货地址不一定是实体。需要通过业务对象是否需要被追踪其生命周期来区别两者。

上图,每行作为一个值对象,当“66街道”变为“金牛街道”时,原值对象变为了另一个值对象,实质是两者没有一一对应,系统不能判定为同一对象。

实际是,在数据库存储中,为了找到唯一确定的数据值,均会为数据表内的每条数据加上ID。

上图,每行是一条数据表信息,通过唯一 ID找到此条数据,查看或应用,但不支持编辑。它有 ID但系统不需要跟踪它的生命周期,它不是实体。可能由于业务的特殊性,被当作地址字典仅供查看和使用。

下述情况,“收件地址”被看作实体。

系统支持一个用户同时维护多个地址,这时详细地址就是实体。如常见收件地址,一个用户最多创建5条,支持修改和删除。用户已创建5条地址,下单商品时确定收货地址A,两天后,需要变更地址,此时变更地址后,物流系统需要知道这个地址变更的完整轨迹(原地址是什么?新地址是什么?谁改的?什么时候改的?),用于物流成本核算、风险控制或客服仲裁。

4、聚合V聚合根

业务规则到系统功能/数据关系的映射,需要识别事务边界。

(1)事务边界

在数据库层面:一个事务可能包含更新3张表,要么都提交,要么都回滚。

在 DDD 聚合层面:一个聚合就是一条事务边界。这意味着,当你修改一个聚合内部的任何部分,整个聚合的所有变化必须在同一个数据库事务中完成。

例如,订单商品的数量变更,订单表数据修改,商品库存数据表修改,商品物流数据表修改。当订单商品的数量增加了n时,与商品“数量”有关的内容均需要变更,即商品库存需要减少n,物流发货时商品数量需要增加n。

(2)聚合V聚合根

在上述例子中,“订单”“存库”“物流”是一组强关联的业务对象的集合,它们被称为聚合,在整体上需要保持一致性;在这个聚合中,“订单”是聚合根,要修改 “收货地址”,都必须通过“订单”这个聚合根来进行。

聚合与聚合根,体现数据表间的作用是保持数据一致性。数据表A数据a变更影响哪些数据表数据变更,数据表B数据b变更需要从数据表A的某数据开始变更。

加深:聚合V聚合根

实例讲述

订单有,商品(商品名称、商品数量)、收货地址。

订单的商品数量改变“通过订单影响库存”(订单→库存),订单的收货地址改变“通过订单影响物流” (订单→物流),以上两项由于订单信息改变影响了下游的相关模块信息的改变。

商品名称通常在系统中买家不能更改名称,但存在卖家把名称改变,同时需要更改订单内商品名称的情况(商品-商品名称→订单),此时订单属于下游。

核心原则

聚合是用来保证“写操作”的事务一致性的,而不是用来建模“读操作”或“通知依赖”的。

例如:

订单聚合保证的是:修改订单项数量时,订单总价必须同步重算。

订单聚合需要“通知”库存系统。这个“通知”不需要在同一个事务中,可以异步,理解为“一个事情发生另一个事情需同步发生”。

实例剖析

场景1:订单商品数量改变 -> 影响库存(依赖关系:弱,适合异步)

库存和订单,最好不在同一聚合。

原因:

库存和订单是两个完全独立的业务概念,有不同的生命周期、规则和负责人。库存系统关心的是“仓库里还剩几件货”,订单系统关心的是“用户买了什么”。把库存放进订单聚合,会导致订单事务变得极其笨重。

合理设计:

(1)订单聚合内修改商品数量,重新计算总价。

(2)订单聚合发布OrderItemQuantityChanged领域事件。

(3)库存系统监听领域事件,异步调整预留库存。

业务:可接受的短暂不一致。

场景2:订单收货地址改变 -> 影响物流(依赖关系:中,适合异步 + 状态约束)

库存和订单,可以不在一个聚合。

原因:

物流是另一个独立的流程,有自己复杂的路由、配送员、轨迹跟踪,但有额外约束。如果订单已经“已发货”,通常不允许修改地址。

合理设计:

订单聚合执行,在order.changeAddress()方法中,检查order.status是否为“待发货”。如果是“已发货”,抛异常。

物流系统无需感知。物流系统只需要监听OrderAddressChanged领域事件,如果收到一个已经发货的订单的改址事件,它可以记录告警或忽略。

订单聚合负责保护自己的不变性(“已发货不可改地址”),物流系统作为下游响应变化。

场景3:卖家修改商品名称 -> 影响历史订单(依赖关系:反向,最棘手)

卖家改了商品名称,需要同步更新所有包含该商品的订单里的商品名称快照。

关键:订单中的商品名称,本质上是值对象,而且是“历史快照”。

问题:卖家把“iPhone 14”改名为“iPhone 14(翻新机)”后,卖家需能够辨别订单上的商品,更名前后为同一商品。用户看自己半年前买的订单,却发现名称变了。这违背了订单作为法律凭证的不可篡改性。

合理设计:

订单项中存储的是快照值对象:productNameSnapshot、priceSnapshot、productId(仅用于追溯)。

创建订单时,从商品服务获取当前名称,复制一份存进订单项。

卖家修改商品名称,不影响任何已有订单,历史订单保持创建时的名称。若仅卖家需要查看商品新名称,后台展示订单可以通过“原名称”“新名称”进行展示。

如果业务上确实需要同步更新(比如促销活动改名,希望用户看到新名称),这是一个明确的、显式的批量更新操作,而不是一个领域事件。

订单是商品名称变更的 “历史记录者”。

领域事件

在上述例子中,提到了“领域事件”,这里简单解释说明(不过多陈述)。

领域事件定义:聚合之间的消息传递机制就叫领域事件。

扩展:在聚合根的生命周期中,发生的其他聚合(或外部系统)需要知道的、重要的事实。

关键词1:事实。它描述的是“已经发生的事情”。比如“订单已支付”是事实,“请支付订单”不是。

关键词2:需要知道。只有对业务有意义的、其他部分关心的事件才需要发布。比如“订单的lastModifiedTime字段更新了”通常不是领域事件。

通过领域事件进行消息沟通的聚合、系统等,可以异步执行、处理工作。

本文由 @产品-子鱼 原创发布于人人都是产品经理。未经作者许可,禁止转载

题图来自Unsplash,基于CC0协议

更多精彩内容,请关注人人都是产品经理微信公众号或下载App
评论
评论请登录
  1. 目前还没评论,等你发挥!