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字段更新了”通常不是领域事件。 通过领域事件进行消息沟通的聚合、系统等,可以异步执行、处理工作。 内容来于网络,如有侵权,请联系管理员删除! |
关注公众号相关侵权、举报、投诉及建议等,请发 E-mail:beetec@163.com
Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.|浙ICP备20023745号-1