DDD 实战 (8):冲刺 1 战术之聚合设计

本篇开始我们对“群买菜”首个冲刺的战术设计进行描述。上篇《DDD 实战 (7):战术设计、整体流程与首次冲刺》中,我们已经识别了首个冲刺的 14 个业务用例和 23 个服务契约的识别,并分别给出了相应的业务用例规约和服务契约设计。下面我们分两篇来分别完成:1)按照 14 个业务用例规约完成聚合设计;2)按照 23 个服务契约,在聚合设计的基础上,完成服务设计(含应用服务、领域服务);3)作为首个冲刺,完成必要的战术层面相关技术决策(这一步工作一般只在首个冲刺的时候会做,后面的冲刺可能会有补充完善)。

3. 首个冲刺的概念模型与聚合划分

本篇就先完成第一个工作:基于 14 个业务用例规约完成聚合设计。对于每个上下文来说,其实我们按照如下的 4 步走的“快速建模法”来完成聚合设计:

  1. 名词建模。这一步其实就是查看该上下文的所有业务用例,从其中识别出所有的“名词”,包括那些带定语的名词,并初步建立这些名词之间的关联关系。

  1. 动词建模。动词建模的主要目的,是为了发现“时标对象”。时标对象我们可以这样来理解:它是用来记录在某些关键时刻涉及到管理责任、法律纠纷或财务风险的“过程性记录”。这种记录的真正作用,并不是业务本身所需要的,而更多是从企业管理角度来考虑的。下图展示了“时标对象”的概念定义和识别方法(绿勾即为时标对象,而红叉则不是):

  1. 归纳抽象。在完成了名词建模、动词时标对象识别后,即可以对对象模型进行抽象归纳,并识别出哪些是值对象、哪些是实体对象。这一般包括这些工作:

  • 1)通过合并同类项,主要是那些定语修饰的不同名词、其实是一个对象类的情况(如:配送地址、家庭地址等,这种属于定语引起的值的差异);

  • 2)通过定语识别出新的对象,主要是那些定语修饰的不同名词、其实是不同类型的情况(如:订单状态、商品状态);

  • 3)去掉一些没有必要存在的对象,比如:没有业务意义的名词、其实可以使用语言基本类型的名词等;

  • 4)区分值对象、实体对象。按照一个基本的原则来识别,即:是否对象的所有属性相同,但仍然可被认为是不同的对象,这种情况必须要有标识 ID 才能区分不同。

  • 5)确定实体对象之间的关系,包括:泛化、关联、依赖。泛化是父类子类之间的关系;关联是对象的属性中引用另一个对象,又包括合成关系(A 由 B 合成,表示 B 为 A 的组成部分,并且 B 存亡依赖于 A 的存亡,如学校和班级的关系)、聚合关系(A 由 B 聚合,表示 B 为 A 的组成部分,但 B 存亡并不依赖于 A 的存亡,如班级和学生的关系)、普通关联(即 A、B 之间的普通属性引用关系,允许 1 对 1、1 对多、多对多);依赖是方法出入参引用到另一个对象。

一般来说,对象模型的建立,采用“多比少好”的基本原则。

  1. 划分聚合。将一个上下文中的多个实体对象进行聚合划分。一般来说,我们本着“小聚合”的原则,区分聚合的唯一判定规则:该实体对象是否存在从用户角度被直接查询和处理的必要。例如:订单里面的订单项,用户就没必要跳过订单、而直接查询订单项的必要,这种情况下“订单项”就作为“订单”聚合的内容,而不需要单独作为聚合存在。之所以这么做,是因为按照我们的菱形架构,对于数据资源类的端口采用“资源库”接口来实现,而一个聚合对应一个资源库。所以,如果不需要为某个实体对象单独开发“资源库”端口(及对应的适配器类),就没必要将其作为独立的聚合。

3.1 鉴权上下文

3.1.1 名词建模

根据用例“登录系统”规约查找名词:微信 openid、微信授权信息、授权记录、用户 ID、有可管理店铺标记、有可管理接龙标记、店铺 ID、位置、距离。

3.1.2 动词建模(时标对象)

重大时刻:登录系统

可能的过程性记录:登录日志

是否关联到管理责任、法律纠纷、财务风险:考虑到用户微信可能被盗用,而且“群买菜”是个双边开放平台,允许任何人注册店铺和销售商品,为了规避法律和财务风险,故有必要将登录日志保存下来。为此,识别出“登录日志”这一时标对象。

3.1.3 归纳抽象

根据如上识别的所有对象,我们绘制概念模型图如下:

对上图进行相应的归纳抽象后,我们发现:

1. “授权记录”其实就是在我们“群买菜”系统的“用户”,其实质是微信用户在授权登录后在“群买菜”系统的一对一映射。这样“用户 ID”其实就是“用户”对象的标识。所以,“用户 ID”应该是值对象。

2.  “店铺”其实是“店铺上下文”的实体对象,授权上下文只关心“店铺 ID”,由于跨上下文,故只需要作为“用户”实体对象的“计算性属性”(最近一次浏览的店铺 ID、或距离最近店铺 ID),且使用基本类型 String 即可。

3. “微信 openid”应该作为“用户”实体对象的属性。考虑到它实际上是一种特定平台、特定格式、特定含义的字符串,故设计为值对象。

4. “登录令牌”是依附于“用户”存在的,并且每次登录后都被更新掉,没有自身标识 ID 存在的必要,故也作为值对象存在。

5. “微信授权信息”是个“瞬态信息”,可以作为值对象存在。

6. “位置”。由于我们并不是一个物流或地图类应用,不需要对位置进行精确的匹配,所以作为值对象。并且,在我们的“授权上下文”中,其应该是用户对象在某个时刻的一个计算属性(根据手机定位计算)。

7. “距离”。这是从经纬度计算出来的一个整数或浮点数(视采用的计算单位而定),但它有特定的业务含义,故设计为值对象。同时,它是根据位置进行计算的,所以它和“位置”值对象之间是一种方法调用上的“依赖”关系(“距离”对象会使用“位置”对象来构建自身)。

8.  “有可管理店铺标记”、“有可管理接龙标记”,这明显是两个计算属性(根据该用户是否被店铺创建人授权、是否创建接龙等计算),可以作为“用户”实体对象的属性存在。考虑到这类标记性属性,会随着“群买菜”系统业务逻辑的演变、以及前端展示需求的变化等需要,我们可以设计一个“用户状态”值对象类。

9. “登录日志”应该是实体对象,且“用户”和“登录日志”之间应该是“合成”关系(后者是因为前者存在而存在的)。

根据上面的归纳抽象,再考虑用英文表达对象名称,我们修改概念模型图如下(值对象用阴影表示,箭头表示单向关联,实心菱形表示合成关系,空心菱形表示聚合关系,无箭头实线表示双向关联)

3.1.4 划分聚合

本上下文只有两个实体对象:用户、登录日志。唯一要回答的问题是:“登录日志”是作为“用户聚合”的内容、还是独立聚合存在?这取决于业务上有没有不需要通过“用户”实体对象而直接访问“登录日志”的需求场景。从实际需求来说,“登录日志”是动词时标对象,我们前面做分析时已经意识到记录它的目的是为了方便以后的财务、法律风险核查,也就是说可能会开发针对“群买菜”平台后端运营的相关功能,而这些功能是可能是直接查询某个时间段、或满足某登录地理位置范围等审计条件下的“登录日志”,而并不需要通过“用户”对象来访问它。为此,我们将聚合划分如下图(图中<<AR>>标记表示是“聚合根”):

上图中,需要说明的是:考虑到“位置”和“距离”与业务的完全无关性,建议将“Location”和“Distance”两个类放到“共享内核”上下文中,不归属到某个聚合。

3.2 订单上下文

3.2.1 名词建模

根据各业务用例规约查找名词如下表:

我们将上表的所有名词对象进行汇总,得出如图所示的概念模型:

3.2.2 动词建模(时标对象)

对订单上下文各业务用例的时标对象分析如下表:

总结起来,对订单上下文动词建模新增的对象有:“微信支付结果”、“订单操作日志”。调整后的对象模型如下图:

3.2.3 归纳抽象

现在我们来对这个初步的对象概念模型进行归纳抽象,首先我对某些对象的存在必要性进行分析如下:

1. “商品”、“商品有货状态”、“售罄商品”都属于“商品上下文”的内容,我们在这里作为考虑。

2.  “订单生效事件”、“订单确认事件”属于菱形架构中“南向网关”部分,不属于核心领域,可以不作为对象模型考虑。

3.  “订单状态通知订阅”、“订单状态通知消息”按照上下文职责划分属于“平台集成上下文”,也不在这里考虑。

4.  “订单列表”其实就是“订单”对象的一种 List,且仅用于前端界面查询显示,不需要作为对象模型考虑。

5.  “购物车商品列表”其实是购车中保存的商品信息、下单份数、计量数量(比如:胡萝卜 0.5 斤一份,下单了 3 份就是 1.5 斤)、下单金额小计等信息,故改名为“购物车商品行”。

修改后的对象概念模型如下图:

其次,我们识别这个模型的值对象、实体对象,并适当地再次提炼归纳。分析如下:

  1. “购物车商品总数”其实就是个普通整数,并没有特定的业务逻辑需求,故将其从对象模型删除。
  2.  “购物车待结算总价”、“订单支付金额”其实就是 Money 值对象类,没必要专门为其设定特定的值对象类,故统一为“金额 Money”值对象。

  3.  “购物车商品小计”、“购物车商品分类计数”,这两个是有结构性的、多字段属性的值对象,作为“购物车”和“购物车商品行”的属性存在,而无需作为带标识 ID 的实体对象存在。

  4.  “订单状态”、“订单可见状态”、“订单支付状态”这 3 个对象明显也是作为值对象而存在。因为其分别作为“订单”和“订单支付记录”的属性而存在,并且需要定义特定的业务取值,并不能无限任意取值,所以需要有自身的业务逻辑知识。

  5.  “订单提货方式”、“订单联系信息”、“订单送货地址”这 3 个对象也适合作为值对象而存在。其中,“订单提货方式”具备取值范围的业务知识;而“订单联系信息”、“订单送货地址”是多属性字段组合的整体概念,因为“群买菜”不是物流类软件,没必要将它们识别为具备 ID 标识的实体对象,所以它们也都作为值对象存在。

  6.  “订单备注”目前只是一段普通文本,其实是可以作为基本数据类型的,但考虑到将来的业务扩展性,可能会出现格式化要求。况且,即使现在我们也是需要限制其字符串长度的,这也算是一种“业务知识”。为此我也将其作为值对象考虑。

  7.  “手机号”是一种特定格式和取值范围要求的数字字符串,故也作为值对象。

  8.  “订单”、“订单行”、“订单商品快照”、“订单支付记录”这 4 个对象,是需要有 ID 标识存在的实体对象。很显然,一方面这些对象是具备多个字段属性的结构体,另一方面它们的“唯一性判别”不是基于其属性取值而是基于 ID 标识的。所以它们都是实体对象。

  9.  “品牌商品”可以理解它是品牌商店铺的商品库中某个商品、被加盟商销售后,形成在订单下的一种特殊的“订单商品快照”。“品牌商品”这个说法,只有在特定“订单”记录里面才生效。所以说,其实“品牌商品”是“订单商品快照”的一个子类,所以它也是一种实体对象。

  10.  “品牌商子订单”是在客户确认订单收货后,系统为品牌商品关联的品牌店铺自动生成的子订单,所以也是一种实体对象。不过,它属于“订单”的子类。但同时,“品牌子订单”又需要关联到“订单”作为其父订单,故“品牌子订单”和“订单”实体之间就有两重关系:泛化关系、关联关系。

  11.  需要说明的是:“品牌商品”、“品牌商子订单”不属于本次冲刺的工作范围。

  12.  “订单支付完成时间”可以使用 java 语言的基本数据类型 TDateTime 或 TLocalDateTime 即可,因为其取值并没有限制,故从对象模型中删除。

  13.  “微信预支付订单”其实是微信支付平台返回的、一系列用来给微信小程序前端调起微信支付的参数组合。它是依附在订单支付记录上的,随着微信支付的成功与否而更新内容,因此它也可以作为值对象存在。

  14.  “微信支付结果”类似“微信预支付订单”,它是微信支付平台返回的支付结果信息,也可以作为依附于订单支付记录而存在的值对象。

  15. “订单”是记录订单操作痕迹的,是用来记录订单包含客户创建、客户支付、商家备货发货、客户确认、订单取消等一系列的信息,跟订单对象之间是合成关系,需要作为实体对象考虑。

  16. “订单支付时限”、“订单确认时限”这两个看起来是某个对象。但实际上,它们是某种业务规则,是用来在订单创建是创建该订单的支付截止时间、确认截止时间。所以说,这两个名词更像是某种业务规则的配置参数。对于这种情况,有两种处理方式:一种是设立“规则上下文”并引入规则引擎,将它们全部纳入规则引擎的设计框架下,不再遵循 DDD 思想对其进行设计;另一种是将其转化为某种 DDD 对象模型。考虑到“群买菜”前面的战略设计中,已经舍弃规则引擎的引入,所以我们采用第二种处理方式。鉴于“订单支付时限”、“订单确认时限”实际上是某种业务参数配置,为了通用性,我们在对象模型中引入“业务参数”实体对象,该实体对象的 ID 即为“参数编码”,用于区分获取不同的业务配置参数。这样,就将“订单支付时限”、“订单确认时限”作为某种“参数编码”的“业务参数”来看待,而计算订单支付截止时间、确认截止时间的业务逻辑则由“订单上下文”的相关领域服务来实现。

  17.  “订单剩余支付时限”是用来给客户提示支付剩余时间的。显然,它是一种“计算结果”,是根据订单支付截止时间(见上条)结合系统时钟自动读秒倒计时的。事实上,这是一个“瞬间”取值,仅用于前端界面提示客户支付,并没有其它的业务价值,而且技术上并不适合要求后端服务给出准确的计算结果(会导致大量的前后端交互)。更为可取的方法,还是由前端界面根据“订单创建时间”进行计算。当然,这样做的坏处是:前端界面具备的一定的业务知识。但考虑到在“设计价值”和“实现简便性”上的权衡,我们还是建议这部分计算在前端界面实现。为此,从对象模型删除该对象。

经过上面的提炼归纳,我们调整订单上下文的对象模型如下图:

需要说明的是:“订单行”与“订单商品快照”之间其实是合成关系。因为订单商品快照依赖于订单行管理其生命周期,也就是因为订单行的存在而存在。但这里的“合成”关系有点特殊:一个订单行只会有一个商品快照。

为了方便代码实现,我们将对象模型的中文名改为英文名,如下图:

3.2.4 划分聚合

事实上,上面的对象模型已经基本将聚合划分清晰了。唯一需要考虑的,就是“品牌子订单”需要和“订单”这两个实体对象分开在不同的聚合中,因为“品牌子订单”对于品牌商来说,是需要有独立的访问入口的(如:查询某品牌商收到的子订单),故在聚合上必须区分开来。划分聚合后的对象模型如下图:

需要说明的是:Money、Visible、MobileNumber 放到“共享内核上下文”,不作为任何聚合的内容。

3.3 商品上下文

3.3.1 名词建模

根据各业务用例规约查找名词如下表:

需要说明的是:“店铺”属于店铺上下文、“购物车”、“购物车状态标记”属于订单上下文,这里不作为考虑范围。商品上下文根据名词初步建模的对象模型如下图:

3.3.2 动词建模(时标对象)

由于冲刺 1 只涉及到查询类用例,故没必要分析时标对象。

3.3.3 归纳抽象

我们对上面的商品上下文对象模型做归纳抽象,并去掉一些没必要的对象。分析如下:

  1.  “售罄商品”其实是商品的一种,在我们已经有“商品有货状态”对象后,这个对象就显得多余,故去掉。
  2.  “关键词列表”其实就是“关键词”的 List,没必要单独出来一个对象,去掉。
  3.  甚至“关键词”都可以直接使用 java 语言的基本类型 String,暂时还没有诸如关键词热度、关联属性等特定业务规则,也可以去掉。
  4.  “商品最小下单量”就是普通的浮点数,不作为对象模型。
  5.  “商品显示顺序”、“商品类别显示顺序”都可以视作普通的整数,不作为对象模型。

我们再来对该对象模型识别识别值对象、实体对象,并给对象加上英文名称。分析如下:

  1. “商品名称”、“商品描述”其实是受限制的字符串类型,显然作为值对象;
  2.  “商品图片”并不会独立存在于“群买菜”系统中,而总是依附于商品存在,故也作为值对象,并改名为“图片”;
  3. “商品定价”其实就是一个金额,取值没有限制、也没有特别的业务逻辑,直接改为使用 Money 值对象。
  4.  “商品计量单位”、“商品优惠”、“商品限购”、“商品有货状态”显然也不会独立存在,而总是依附于商品存在,故也作为值对象。
  5. “商品月销量”对每个商品来说,需要每月统计,而且它是依附在“商品”上存在的(“商品”控制了其生命周期),所以它是一种和“商品”之间有“合成”关系的实体对象。

最终修改后的对象模型如下图:

3.3.4 划分聚合

该对象模型中,需要区别直接访问入口的实体对象有“商品类别”、“商品”。“商品类别”需要单独访问是需要在前端界面支持列出所有的商品类别,“商品”需要单独访问入口显而易见。而其它实体对象“商品图片”、“商品月销量”是不需要单独访问入口的,故最终聚合划分如下图:

需要说明的是:Money 仍然使用“共享内核”上下文的对象。Image 因为与业务完全无关,而且大概率会被其它上下文用到,也放到共享内核上下文。

3.4 平台集成上下文

平台集成上下文,仅仅是微信开放接口的一些简单封装,包含“获取微信绑定手机号”、“保存订单状态通知订阅”、“向店铺发送订单状态通知”等简单功能。这些业务逻辑,基本上没有太多“领域”知识,正如我们在战略技术决策中考虑的,不考虑对其进行 DDD 战术设计。

到此,我就完成了“群买菜”冲刺 1 战术设计的聚合设计部分,剩下我还会用 1~2 篇完成冲刺 1 的服务设计战术层面技术决策,然后就开始实际的 TDD 编码实现了。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340