本篇开始我们对“群买菜”首个冲刺的战术设计进行描述。上篇中,我们已经识别了首个冲刺的 14 个业务用例和 23 个服务契约的识别,并分别给出了相应的业务用例规约和服务契约设计。下面我们分两篇来分别完成:1)按照 14 个业务用例规约完成聚合设计;2)按照 23 个服务契约,在聚合设计的基础上,完成服务设计(含应用服务、领域服务);3)作为首个冲刺,完成必要的战术层面相关技术决策(这一步工作一般只在首个冲刺的时候会做,后面的冲刺可能会有补充完善)。
01首个冲刺的概念模型与聚合划分
本篇就先完成第一个工作:基于 14 个业务用例规约完成聚合设计。对于每个上下文来说,其实我们按照如下的 4 步走的“快速建模法”来完成聚合设计:
1. 名词建模。这一步其实就是查看该上下文的所有业务用例,从其中识别出所有的“名词”,包括那些带定语的名词,并初步建立这些名词之间的关联关系。
2. 动词建模。动词建模的主要目的,是为了发现“时标对象”。时标对象我们可以这样来理解:它是用来记录在某些关键时刻涉及到管理责任、法律纠纷或财务风险的“过程性记录”。这种记录的真正作用,并不是业务本身所需要的,而更多是从企业管理角度来考虑的。下图展示了“时标对象”的概念定义和识别方法(绿勾即为时标对象,而红叉则不是):
3. 归纳抽象。在完成了名词建模、动词时标对象识别后,即可以对对象模型进行抽象归纳,并识别出哪些是值对象、哪些是实体对象。这一般包括这些工作:
- 通过合并同类项,主要是那些定语修饰的不同名词、其实是一个对象类的情况(如:配送地址、家庭地址等,这种属于定语引起的值的差异);
- 通过定语识别出新的对象,主要是那些定语修饰的不同名词、其实是不同类型的情况(如:订单状态、商品状态);
- 去掉一些没有必要存在的对象,比如:没有业务意义的名词、其实可以使用语言基本类型的名词等;
- 区分值对象、实体对象。按照一个基本的原则来识别,即:是否对象的所有属性相同,但仍然可被认为是不同的对象,这种情况必须要有标识 ID 才能区分不同。
- 确定实体对象之间的关系,包括:泛化、关联、依赖。泛化是父类子类之间的关系;关联是对象的属性中引用另一个对象,又包括合成关系(A 由 B 合成,表示 B 为 A 的组成部分,并且 B 存亡依赖于 A 的存亡,如学校和班级的关系)、聚合关系(A 由 B 聚合,表示 B 为 A 的组成部分,但 B 存亡并不依赖于 A 的存亡,如班级和学生的关系)、普通关联(即 A、B 之间的普通属性引用关系,允许 1 对 1、1 对多、多对多);依赖是方法出入参引用到另一个对象。
一般来说,对象模型的建立,采用“多比少好”的基本原则。
4. 划分聚合。将一个上下文中的多个实体对象进行聚合划分。一般来说,我们本着“小聚合”的原则,区分聚合的唯一判定规则:该实体对象是否存在从用户角度被直接查询和处理的必要。例如:订单里面的订单项,用户就没必要跳过订单、而直接查询订单项的必要,这种情况下“订单项”就作为“订单”聚合的内容,而不需要单独作为聚合存在。之所以这么做,是因为按照我们的菱形架构,对于数据资源类的端口采用“资源库”接口来实现,而一个聚合对应一个资源库。所以,如果不需要为某个实体对象单独开发“资源库”端口(及对应的适配器类),就没必要将其作为独立的聚合。
02鉴权上下文
1名词建模
根据用例“登录系统”规约查找名词:微信 openid、微信授权信息、授权记录、用户 ID、有可管理店铺标记、有可管理接龙标记、店铺 ID、位置、距离。
2动词建模(时标对象)
重大时刻:登录系统
可能的过程性记录:登录日志
是否关联到管理责任、法律纠纷、财务风险:考虑到用户微信可能被盗用,而且“群买菜”是个双边开放平台,允许任何人注册店铺和销售商品,为了规避法律和财务风险,故有必要将登录日志保存下来。为此,识别出“登录日志”这一时标对象。
3归纳抽象
根据如上识别的所有对象,我们绘制概念模型图如下:
对上图进行相应的归纳抽象后,我们发现:
“授权记录”其实就是在我们“群买菜”系统的“用户”,其实质是微信用户在授权登录后在“群买菜”系统的一对一映射。这样“用户 ID”其实就是“用户”对象的标识。所以,“用户 ID”应该是值对象。
“店铺”其实是“店铺上下文”的实体对象,授权上下文只关心“店铺 ID”,由于跨上下文,故只需要作为“用户”实体对象的“计算性属性”(最近一次浏览的店铺 ID、或距离最近店铺 ID),且使用基本类型 String 即可。
“微信 openid”应该作为“用户”实体对象的属性。考虑到它实际上是一种特定平台、特定格式、特定含义的字符串,故设计为值对象。
“登录令牌”是依附于“用户”存在的,并且每次登录后都被更新掉,没有自身标识 ID 存在的必要,故也作为值对象存在。
“微信授权信息”是个“瞬态信息”,可以作为值对象存在。
“位置”。由于我们并不是一个物流或地图类应用,不需要对位置进行精确的匹配,所以作为值对象。并且,在我们的“授权上下文”中,其应该是用户对象在某个时刻的一个计算属性(根据手机定位计算)。
“距离”。这是从经纬度计算出来的一个整数或浮点数(视采用的计算单位而定),但它有特定的业务含义,故设计为值对象。同时,它是根据位置进行计算的,所以它和“位置”值对象之间是一种方法调用上的“依赖”关系(“距离”对象会使用“位置”对象来构建自身)。
“有可管理店铺标记”、“有可管理接龙标记”,这明显是两个计算属性(根据该用户是否被店铺创建人授权、是否创建接龙等计算),可以作为“用户”实体对象的属性存在。考虑到这类标记性属性,会随着“群买菜”系统业务逻辑的演变、以及前端展示需求的变化等需要,我们可以设计一个“用户状态”值对象类。
“登录日志”应该是实体对象,且“用户”和“登录日志”之间应该是“合成”关系(后者是因为前者存在而存在的)。
根据上面的归纳抽象,再考虑用英文表达对象名称,我们修改概念模型图如下(值对象用阴影表示,箭头表示单向关联,实心菱形表示合成关系,空心菱形表示聚合关系,无箭头实线表示双向关联)
「张逸按:我通常将以下两个步骤——分辨实体和值对象;确定实体之间的关系——放到领域设计建模的过程中,在快速建模过程中,不建议介入软件设计的要素,我希望由领域专家(可以不懂软件设计)来主导这一过程。」
4划分聚合
本上下文只有两个实体对象:用户、登录日志。唯一要回答的问题是:“登录日志”是作为“用户聚合”的内容、还是独立聚合存在?这取决于业务上有没有不需要通过“用户”实体对象而直接访问“登录日志”的需求场景。从实际需求来说,“登录日志”是动词时标对象,我们前面做分析时已经意识到记录它的目的是为了方便以后的财务、法律风险核查,也就是说可能会开发针对“群买菜”平台后端运营的相关功能,而这些功能是可能是直接查询某个时间段、或满足某登录地理位置范围等审计条件下的“登录日志”,而并不需要通过“用户”对象来访问它。为此,我们将聚合划分如下图(图中<<AR>>标记表示是“聚合根”):
上图中,需要说明的是:考虑到“位置”和“距离”与业务的完全无关性,建议将“Location”和“Distance”两个类放到“共享内核”上下文中,不归属到某个聚合。
03订单上下文
1名词建模
根据各业务用例规约查找名词如下表:
我们将上表的所有名词对象进行汇总,得出如图所示的概念模型:
2动词建模(时标对象)
对订单上下文各业务用例的时标对象分析如下表:
总结起来,对订单上下文动词建模新增的对象有:“微信支付结果”、“订单操作日志”。调整后的对象模型如下图:
3归纳抽象
现在我们来对这个初步的对象概念模型进行归纳抽象,首先我对某些对象的存在必要性进行分析如下:
“商品”、“商品有货状态”、“售罄商品”都属于“商品上下文”的内容,我们在这里作为考虑。
“订单生效事件”、“订单确认事件”属于菱形架构中“南向网关”部分,不属于核心领域,可以不作为对象模型考虑。
“订单状态通知订阅”、“订单状态通知消息”按照上下文职责划分属于“平台集成上下文”,也不在这里考虑。
“订单列表”其实就是“订单”对象的一种 List,且仅用于前端界面查询显示,不需要作为对象模型考虑。
“购物车商品列表”其实是购车中保存的商品信息、下单份数、计量数量(比如:胡萝卜 0.5 斤一份,下单了 3 份就是 1.5 斤)、下单金额小计等信息,故改名为“购物车商品行”。
修改后的对象概念模型如下图: