上篇《DDD 实战 (6):战略设计之技术决策》已经完成了“群买菜”的全局性(“全局性”的意思,是说整个系统只会有一次)战略设计。从本篇开始,我们就要进入实质性的系统实现过程了。这主要包括战术设计和代码实现。
由于整个群买菜的 DDD 实现工作量不小,我将引入 scrum 敏捷过程将整个系统的开发进行拆分。在接下来关于 DDD 战术设计和代码实现的描述中,我将采用下面的步骤来描述第一个 sprint(冲刺):
-
首先,我简要描述下战术设计包含什么,然后从 DDD 整体过程角度描述 DDD 与 scrum 如何结合——本篇内容。
-
其次,我在本篇中完成第一个 sprint 的相关准备(用例规格和服务契约设计)。
-
然后,我从下篇开始给出第一个 sprint 的战术设计,以及相关战术层面的技术决策。
-
最后,我会给出第一个 sprint 的代码实现。这里面,我又会引入 TDD(测试驱动开发)方式进行编码过程描述。
如上的内容,估计我会用 3~5 篇的内容来描述(更新过程会历时 1 个月以上)。
注:由于内容越来越多,相同的标题很容易导致可读性很差,我就把前面几篇的标题改了下,除了序列标号外,我还在标题中点明了本篇主要内容是什么。这样更方便您选择性阅读。
1. 战术设计与敏捷开发
1.1 战术设计做什么
一般来说,DDD 战术设计其实主要就是给出如下 3 方面的产物(以下内容简要了解即可,现在不用深入理解,后面我会用实例演示怎么做):
-
对象模型识别。说白了,就是根据业务用例需求规格描述,识别出系统中所有的“对象”类、以及它们之间的逻辑关系(泛化、依赖、关联等)和数量关系(1 对 1、1 对多、多对多等)。其实这些方法都是原来 UML 的传统方法。DDD 带来的设计变化是:我们要区分出“实体对象”和“值对象”。
-
实体对象——需要数据生命周期管理的、根据 ID 标识而不是属性来判断是否同一个对象的类。如:订单、订单行等。
-
值对象——不需要数据生命周期管理的(往往作为实体对象的属性存在)、只要属性发生变化就是另一个对象的类。如:地理区域(含省市区)、家庭住址(含经纬度定位和详细地址)、身份证号(含编码规则)、姓名(含姓+名)。
-
聚合设计。在实体对象中,有些实体对象是不需要单独出现的、总是跟着另一个实体对象的出现而出现、消亡而消亡的,如:”订单行”总是随着“订单”的出现而出现、消亡而消亡。往往我们在完成“对象模型和关系识别”后,列出了很多实体对象,这些对象按照“绑定的存亡关系”可以进行分组,分组后有一个实体对象是唯一的访问入口。这种分组就叫“聚合”,而那个作为唯一的访问入口的实体对象就是“聚合根”——一般情况下,给“聚合”的命名就是“聚合根”对象的名称,如:“订单”聚合可能就包含了“订单”、“子订单”、“订单行”、“商品快照”等多个对象。需要说明的是:值对象往往是“附庸”在实体对象上出现在聚合中的。
-
服务设计。一般来说,DDD 设计下的业务逻辑实现,有两种情况:
-
情况 1:实体对象、值对象都是有行为的(也就是方法逻辑),很多业务逻辑就直接在这两类对象中实现了。也就是说,有了“聚合”(里面包含多个实体对象、值对象)的设计,就可以将很多业务逻辑在“聚合”内部的各个实体对象、以及伴随的值对象中方法逻辑中得到了满足。
-
情况 2:对于那些不能明确划分到单个聚合中去实现的业务逻辑,就需要通过“领域服务”、“应用服务”去实现。
-
具体是在聚合(含多个实体对象、值对象)、还是领域服务或应用服务中实现业务逻辑,我会在“群买菜”的开发实例中演示。
除了上面的主要战术设计内容,我们可能也要做出一些必要的战术层面的技术决策,一般包括:
-
DTO & VO 考量。DTO——data transfer object, 数据传输对象,VO——view object,视图对象。说明如下:
-
DTO 是远程服务调用时用来传输数据的一个模型,后者是用来给前端界面呈现数据所使用的一种模型,本质上 VO 也是 DTO。
-
在 java 开发中,是有一些成熟的技术组件可实现 DTO 效果的,如 MapStruct 等(其它语言如 python 可能也有类似的工具)。在我们的战术设计阶段,需要决策是否引入类似的工具。如果引入,则后续所有的开发中在需要在编码规范中做出统一要求,或者干脆开发一个“代码自动化生成”工具,确保程序员不至于把过多精力放在这种“体力活”代码上。
-
对象持久化考量。DDD 本质上是一种面向对象的设计,是不会对 DB 表结构做特别的设计的,甚至是基于“聚合”和其中的“实体”对象模型来确定 DB 表结构设计的。这就造成了对象模型和数据持久之间的一个转换关系问题,典型的有对象和关系数据库之间的“对象关系映射”(ORM)问题。具体到底怎么实现对象模型的持久化、以及持久化采用什么类库框架,取决于采用的是什么数据库类型(关系型、非关系型、文件系统等),以及采用什么类库框架(JPA/Mybatis/Hibernate 等)。
-
资源库类的实现策略。在 DDD 设计方法中,不采用 DAO 等类似的数据访问对象,而是通过与聚合绑定的资源库(Repository)类来实现数据的 CRUD。当确定了使用的对象持久化类库后,还需要决定如何实现资源库类。一般来说,为了考虑代码复用,建议采用“通用资源库+资源库适配器定制”的模式来实现,不过具体怎么做还取决于架构设计者自身的编程经验、团队已有的资源和技术条件等。
1.2 整体视角看 DDD 过程
为了让您对 DDD 整个工作过程、以及这些过程之间的关系有个全局的认识,也为了方便描述下面将要说起的 scrum 敏捷开发,我这里对 DDD 整体过程给出一个工作流程图(图中战术设计、代码实现是我们后面要讲的内容):
从图中可以看出,我们前面的篇幅已经就需求分析和战略设计的绝大部分工作成果进行了演示,但有两个还没有演示:业务用例规格书(属于需求分析)、服务契约设计(属于战略设计)。
之所以前面没有对这两个工作成果进行演示,是因为它们的工作量特别大,完成它们已经属于“很具体”的工作任务,不适合一次性搞定,需放在 scrum 敏捷过程的多个 sprint(冲刺)中去完成。我在这里对他们先分别简单说明一下:
-
业务用例规格书。这其实是用 UML 描述需求时附带的一份文档。与“业务用例识别”只是 UML 图形列出用例不同,它是对业务用例的一个补充性文字说明,一般长成下图这个样子:
从图中可以看出:“业务用例规格书”分“用例描述、触发事件、基本流程、替代流程、验收标准”这 5 个主要部分进行描述,它就是在描述某个业务用例具体的需求。一般来说,这个工作的完成有两种情况:
-
情况 1 :产品化软件系统。由需求分析人员根据产品经理给出的产品设计写出,实际上一般就是产品经理自己写的,和产品 UI 原型一起作为 PRD(产品需求文档)的内容。
-
情况 2 :定制化软件系统。由需求分析人员根据对客户需求的理解和调研进行编制。这种情况下,一般最好还要再根据客户需求画出软件界面原型图。
-
服务契约设计。这其实是对每个限界上下文“北向网关”(即“应用服务”层)需要对外输出那些“可被调用”的服务接口。这一般是这么得到的:
-
首先,为每个业务用例画 UML 服务序列图。对每个 UML 业务用例,根据用例规格说明的内容,将其转化为 UML 服务序列图。例如,针对上面的“加商品到购物车”业务用例,我们画出如下的 UML 服务序列图:
基于上图,我们就发现“订单上下文”需要给出 2 个服务(即被调用箭头指出来的服务):保存购物车、查询购物车信息,而“商品上下文”需要提供“获取商品信息”服务。当然,需要说明的是:有些情况下,业务用例如此简单,以至于并不需要画 UML 序列图就能够识别出来服务列表——比如:业务用例“查看商品详情”很明显只有“商品上下文”的一个服务“获取商品详情“。
-
其次,按业务用例给出服务契约表。将识别出来的服务接口,确定服务方法签名、识别操作类型、确定实现模式(客户/服务端模式、消息订阅模式等),并将最终结果汇总到如下表所示的“服务契约表”中:
-
最后,将整个限界上下文的服务契约表进行汇总。汇总后的服务契约表,就可以作为限界上下文的“代码开发工作范围”了。
从前面演示的“业务用例规格书”和“服务契约设计”显然可以看出:这两部分工作量是很大的。即使从“群买菜”这个并不庞大的业务系统来看,我们也有 76 个用例,如果我们等待每个业务用例的“用例规格书”、“服务契约设计”都做完后才开始代码开发,显然不符合“敏捷开发”原则的。为此,我们才将这部分工作放到后面来讲。
1.3 DDD 与 scrum 的结合
在真实的软件系统开发过程中,我个人强烈建议采用 scrum 敏捷过程进行开发过程的项目管理,而不要采用传统的 PMI 项目管理体系——因为那个是基于“瀑布模型”,它有一个难以忽视的重大缺点:往往在经历了很长时间的软件开发过程,将成果拿出来后发现不符合市场或用户的需求。真要发生这样的结果,那就悲剧了,所以强烈不推荐使用瀑布模型进行开发。
-
之所以建议采用 scrum 敏捷过程,是因为它有两方面的优点:1)DDD 限界上下文的切分,以及 UML 业务用例的识别,使得快速迭代变得简单易行;2)敏捷过程使得能够更早的向利益干系人“展示产品”,进而更早的、及时的对 DDD 的软件设计进行纠偏,使得不至于“付出辛苦努力”的 DDD 设计全部做完后才发现“差之毫厘谬以千里”——毕竟 DDD 设计不是个小工程、而且特别烧脑细胞,“积重难返”就不好了。
-
这里对 Scrum 敏捷项目管理,我就个人理解做个简要的总结(记住数字序列“2-3-4”即可)。我这里不对 scrum 做详细的介绍,只是简单总结:
-
2 个关键列表。产品列表、冲刺列表。产品列表是产品负责人维护的产品将要实现的目标系统的特性列表;冲刺列表是每次迭代(冲刺,sprint)时,开发团队根据产品列表决定、制定的将要纳入本次 sprint 的工作项目列表(很多来自于未完成的产品特性,不过也有别的配套工作项)。
-
3 个工作角色。产品负责人、开发团队、Scrum Master。产品负责人作为唯一责任人,将决定产品将要拥有哪些特性,并对每次 sprint 实现的产品特性提出建议;开发团队不区分编码、测试、文档,要求大家平等民主,共同为每次的冲刺列表进行负责;Scrum Master 是过程教练,确保多次 sprint 的迭代过程中,工作方式符合 scrum 原则,并在每次 sprint 后的工作过程有所改进。
-
4 个关键会议。冲刺计划会议、每日站会、冲刺评审会议、冲刺回顾会议。冲刺计划会议是每次冲刺(sprint,一般建议 2 周时间为限)开始之前的计划会,用来确定本次冲刺的工作项、评估每个工作项的工作量和风险;每日站会就是每天上班来的第一件事就是花不超过 15 分钟时间,团队成员同步下各自的工作状态、遇到的障碍等;冲刺评审会议,其实是冲刺成果展示和评审会,为了让团队外部的干系人(领导、客户、合作部门等)更好地理解和配合产品开发,以便于对产品开发过程进行反馈、或配套工作提前进行资源协调和安排;冲刺回顾会是冲刺结束时,团队内部的“复盘”会,为了发现本次冲刺中的工作过程中的不足,以便下次冲刺时进行改进。
如果您需要进一步了解 scrum,建议您仔细阅读《scrum官方权威指南》,或者您可以在微信读书上找到的这本书《天天学敏捷:Scrum团队转型记》。
-
很显然,在整个 DDD 设计工作过程中,有些工作是可以通过 scrum 敏捷迭代的,有些是基本上一次性设计就完成的(后续只是完善)。下图我对 DDD 整体工作流程中,建议 scrum 敏捷迭代的工作成果进行了标注——见红色五角星标注。需要说明的是:
-
虽然可敏捷实施的工作成果只有 6 项,但工作量上一般占了整个软件项目工作量(基本约等于研发成本)的 80%甚至 90%以上。
-
这 80%以上的工作量,是可以在其它一次性分析和设计工作(不到软件整体研发成本的 20%甚至更少)完成后,在后面的冲刺(sprint)中反复迭代来完成的,而不建议采用传统的“瀑布模型”进行过程管理。
2. 首个冲刺定义与相关准备
在正式开始群买菜的战术设计、代码实现之前,我们先确定第一个冲刺(sprint1)的工作目标和产品特性列表,并基于此完成相应的业务用例规格细化、和相应的服务契约设计。
2.1 工作目标、产品特性和冲刺列表
我们知道,“群买菜”的主要干系人有:消费者(客户)、商家、平台运营方,而支撑的业务内容主要有:在线零售、在线接龙和加盟分销。从干系人和业务内容两个维度来说,最重要的就是为消费者提供在线零售服务(后面的 sprint 再考虑在线接龙、商家后台管理、平台方资金结算等)。为此,我们设定第一个冲刺的工作目标为:为消费者提供完整的生鲜在线零售服务。
而为了满足这个工作目标,我们需要实现如下的主要产品特性(合计 14 个业务用例):
-
实现客户选择商品所需的“商品上下文”的 3 个业务用例,包括:浏览店铺商品、搜索店铺商品、查看商品详情。
-
支持客户完成完整的在线购物体验相对应的“订单上下文”的 9 个业务用例,包括:加商品到购物车、确认订单付款、生效订单、浏览我的订单、查看订单详情、确认订单完成、超时自动确认订单完成、删除订单、超时自动取消订单。
-
为了支持客户下单相对应的 2 个业务用例:鉴权上下文的“登录系统”、平台集成上下文的“获取微信绑定手机号”。
为了完成第一个 sprint 的工作目标,我们需要完成如下冲刺任务(可以在冲刺进行中,不断的进一步细化和更新冲刺任务列表):
-
细化上述 14 个业务用例规格书;
-
完成上述 14 个业务用例对应的服务契约设计;
-
基于上述 14 个业务用例规格书、服务契约设计,完成 DDD 战术设计;
-
由于是首次冲刺,完成相关的战术层面的技术决策,并开发好相应的代码;
-
作为第一次冲刺,准备好相应的开发测试环境(含 spring boot 开发环境、mysql 数据库、activeMQ 消息中间件等测试环境);
-
完成本次冲刺 DDD 战术设计要求的所有聚合、领域服务、应用服务的代码实现(我这里建议是 TDD 方式编码);
2.2 业务用例规格和服务契约
需要说明的是:后面的业务用例编号规则为“QMC+上下文代号+用例序号”。其中“QMC”是群买菜拼音开头字母,上下文代号列表如下:
下面内容的表格中,需要说明下:“业务服务”和“业务用例”是一个意思,不是属于 DDD 的远程服务、本地服务、应用服务等范畴的实际服务,而只是一种“业务”层面的描述名词。
2.2.1 鉴权上下文相关
1. 登录系统
业务用例规格书细化如下:
该用例的服务序列图如下:
服务契约设计如下表:
2. 获取微信绑定手机号
业务用例规格书细化如下:
该业务用例比较简单,直接出服务契约表如下:
2.2.2 订单上下文相关
1. 加商品到购物车
业务用例规格书细化如下:
该用例的服务序列图如图:
服务契约设计如下表:
2. 确认订单付款
业务用例规格书细化如下:
该用例的服务序列图如下:
服务契约设计如下表:
3. 生效订单
业务用例规格书细化如下:
该用例的服务序列图如下:
服务契约设计如下表:
4. 浏览我的订单
业务用例规格书细化如下:
由于该用例只涉及到订单一个上下文,且没有与外部伴生系统产生关系,且前端与服务端的交互其实只有一次(只是是否包含 3 个月内的限制条件),故无需绘制服务序列图。
服务契约设计如下表:
5. 查看订单详情
业务用例规格书细化如下:
由于该用例只涉及到订单一个上下文,且没有与外部伴生系统产生关系,且前端与服务端只有一次交互,故无需绘制服务序列图。
服务契约设计如下表:
6. 确认订单完成
业务用例规格书细化如下:
该用例的服务序列图如下:
服务契约设计如下表:
7. 超时系统自动确认订单完成
业务用例规格书细化如下:
由于该用例只是“确认订单完成”的批量操作,不过是前面加了一段批量查询出超时订单,故不再绘制服务序列图。
服务契约设计如下表:
注:这里的领域事件通知类的服务功能契约,因为与“确认订单完成”相同,故不再重复列出。
8. 删除订单
业务用例规格书细化如下:
由于该用例只涉及到订单一个上下文,且没有与外部伴生系统产生关系,且前端界面与服务端只有一次交互,故无需绘制服务序列图。
服务契约设计如下表:
9. 超时系统自动取消订单
需要说明的是:在产品 UI 原型设计中,并没有客户手工取消订单的界面,这应该是产品经理认为对于日常买菜这种事情来说,不会有客户需要手工取消订单,故没有设计该功能。
业务用例规格书细化如下:
由于该用例只涉及到订单一个上下文,且没有与外部伴生系统产生关系,且服务端与前端机器人只有一次交互,故不再绘制服务序列图。
服务契约设计如下表:
2.2.3 商品上下文相关
1. 浏览店铺商品
业务用例规格书细化如下:
由于该用例只涉及到店铺一个上下文,且没有与外部伴生系统产生关系,故无需绘制服务序列图。
服务契约列表如下:
2. 搜索店铺商品
业务用例规格书细化如下:
由于该用例只涉及到店铺一个上下文,且没有与外部伴生系统产生关系,故无需绘制服务序列图。
服务契约列表如下:
3. 查看商品详情
业务用例规格书细化如下:
由于该用例只涉及到店铺一个上下文,且没有与外部伴生系统产生关系,故无需绘制服务序列图。
服务契约列表如下:
2.3 按生产者汇总服务契约
为了方便后面的战术设计,尤其是战术设计中的服务设计,我们将各上下文相关的业务用例所识别出的服务契约,按照其生产者上下文进行汇总归类如下:
1. 鉴权上下文服务功能
2. 订单上下文服务功能
3. 商品上下文服务功能
4. 店铺上下文服务功能
需要说明的是:除了“查询店铺信息”外,其它不作为 sprint1 的工作范围。
5. 客户上下文服务功能
需要说明的是:客户上下文的相关服务功能,不作为 sprint1 的工作范围。
6. 接龙上下文服务功能
需要说明的是:客户上下文的相关服务功能,不作为 sprint1 的工作范围。
7. 商家上下文服务功能
需要说明的是:商家上下文的相关服务功能,不作为 sprint1 的工作范围。
8. 平台集成上下文服务功能
需要说明的是:“向店铺发送订单生效通知”、“向店铺发送订单确认通知”服务功能,不作为 sprint1 的工作范围。
从上面汇总的结果来看,总共有 21 个服务契约属于本轮冲刺的工作范围:鉴权上下文 2 个、订单上下文 12 个、商品上下文 5 个、平台集成上下文 1 个、店铺上下文 1 个。
由于接下来的工作量巨大,周期很长,所以本篇先对接下来的战术设计、代码实现工作,引入 scrum 工作方法,并将其与 DDD 工作过程结合起来。同时,本篇还完成了第一次冲刺的任务列表初步定义,以及通过服务契约列表对冲刺任务列表进行了初步的细化,这对于 scrum 来说是必须的准备工作(相当于我们完成了“冲刺计划会议”的工作内容)。在下篇中,我将对首次冲刺的内容进行战术设计。