该服务序列图展示出,实际上“接龙”、“店铺”这 2 个上下文没有发生关联关系。但这个服务序列图设计,有个“坏味道”的感觉:让群买菜小程序客户端承担了过多业务逻辑,这是不合理的。于是,我们将服务序列图调整为如下:
该服务序列图会导致如下的 2 个上下文之间的关系:
确认接龙付款
确认接龙付款从产品界面原型可以看出,确认接龙付款是从“查看接龙详情”界面发起的。客户在该界面上点击相应的商品加入购物车、或从购物车移除,然后点击“我要接龙”按钮进入该用例。
该用例允许用户设置提货方式、提货时间、联系人等信息后,点击“确认付款”按钮完成支付。该按钮点击后,按照产品原型设计,需要完成如下任务:
- 创建订单;
- 更新关联商品的销量,以便于后续商品列表和详情页面显示商品销量;
- 如果下单客户首次购买对应店铺商品,则为该店铺初始化对应客户资料,以便于商家后续维护客户资料;
- 记录客户参与了该接龙,以便于客户“浏览我的接龙”时,可包含该接龙;
根据上面的逻辑,我们画出服务序列图设计如下:
该服务序列图展示的相关限界上下文关系如下图:
这里可以看到上下文之间的调用关系比较多,并且“订单”与“商品”、“订单”与“客户”之间,还存在数据一致性的要求,不利于系统的“松耦合”。为了降低上下文之间的耦合性,我们分析业务需求发现:其实“订单创建”后“增加商品销量”、以及“为指定店铺初始化客户”是可以有一定的数据延迟的,并不需要通过“强”服务调用关系保障严格的数据一致性。为此,我们将服务序列图修改如下:
根据改进后的服务序列图,可以调整上下文映射关系如下图:
结算订单收入
结算订单收入分为两步:第一步,在客户确认订单完成、或机器人超时自动确认订单完成时,订单上下文通知商家账户上下文记录待结算的订单 ID(由于订单上下文缺乏“判断什么订单属于待结算状态”这样的领域知识,故只能由商家账户上下文来记录);第二步,系统机器人定时触发商家账户上下文对待结算订单进行收入结算。服务序列图设计如下:
其中第一步的操作,可以和“发送订单提醒”中的“订单已完成”领域事件合并处理。不过,因为这里是不可丢失的领域事件,所以这就要求采用“可靠事件模式”。基于该服务序列图,识别出“订单”和“商家账户”上下文的关系如图:
结算佣金收入
结算佣金收入可以和结算订单收入同时进行,也可以分开在两个用例中异步执行。考虑到为了将来支持允许品牌商在加盟政策中设置结算周期,故将其分开在两个业务用例异步执行。为此,设计其服务序列图如下:
该序列图展示出商家账户和订单的上下文关系如图:
3限界上下文映射图
我们将上面针对各跨上下文业务用例分析后,得到的上下文映射关系进行汇总后最终得出下图:
图中实线是服务调用关系,属于“强关系”;虚线是消息通知关系,属于“弱关系”。
通过该限界上线文映射关系可以看出:我们总共有 9 个限界上下文,其中有强依赖关系的涉及到 9 个、强依赖关系链有 9 条。分别是:接龙依赖于店铺、商品和订单,店铺依赖于平台集成,订单依赖于商品,商家账户、员工、客户依赖于鉴权,商家账户依赖于订单。9 个上下文,理论上最多有 72 条依赖链条,我们分析汇总后只有 11 条,已经是很好的设计。
基于这样的综合评估,我们认为目前的设计已经到了“可接受程度”,暂时不再做更多的优化和调整了,以避免“过度设计”。
02系统架构和代码框架
1业务子域与上下文的映射
完成了限界上下文的关系映射,其实就有了对整体系统架构进行分层设计的基础。系统整体架构设计从分层角度来看,包括:边缘层、业务价值层、基础层。边缘层一般都是各种针对前端 UI 的控制器,业务价值层和基础层包含所有的限界上下文,其中基础层放的是对应到支撑子域、通用子域的限界上下文,而核心子域对应的限界上下文作为业务价值层。
为了区分业务价值层和基础层,我们先将前面得出的业务子域(见第三篇中全局分析内容)、与限界上下文进行如下表所示的关系映射:
需要说明的是:其中“商家管理”和“店铺管理”虽然是支撑子域,但由于分别被合并到“商家账户”、“店铺”上下文来开发实现,故其中逻辑的代码实现也被划分到了“业务价值层”。
2菱形对称架构简介
在对整个系统进行“分层架构设计”之前,我们还需要先熟悉一个软件架构模式——菱形对称架构(以下简称菱形架构),如下图所示:
对“菱形架构”的说明如下:
菱形架构的基础,是依赖于“领域层”的界定。这涉及到后面才会进行的 DDD 战术设计内容,这里不做展开,您只需要知道:“领域层”放的是业务领域的关键业务知识,包括“聚合(内含实体对象、值对象)”和“领域服务”两类内容。事实上,我们所追求的“高内聚”主要体现的“领域层”,因为业务需求变化而引起的变化,我也希望主要通过“领域层”的“聚合”和“领域服务”的变化来实现。所以说,“领域层”是 DDD 的“核心代码所在地”。
所有非“领域逻辑”层的代码,我们都希望封装到“北向网关”或“南向网关”中去。具体说明如下:
“北向网关”其实就是上下文向外提供服务、或接受消息通知的入口处。如果上下文只需要向外提供同一个进程内部的应用服务调用接口、或接受消息通知(通过消息总线),则需要“本地”服务;如果上下文需要作为独立进程(这时候一般是云原生的独立“微服务”)向外输出服务、或接受消息通知(通过消息中间件),则需要将“本地服务”包装为“远程”服务。
也就是说:北向网关中的“本地服务”和“远程服务”是一一对应的,“远程服务”只负责将远程调用的出入参做格式转换并传给“本地服务”,而“本地服务”负责调用领域层的“聚合”或“领域服务”来完成具体的业务逻辑(关于“聚合”和“领域服务”的内容,我会在本专题后面的章节中演示)。
“南向网关”其实就是上下文用来将这 3 类技术细节从业务逻辑中“剥离”出来的主要手段(图中只画了第一种):访问底层数据库、调用其它上下文服务、向其它上下文发布消息通知。这 3 个技术细节的封装,在 DDD 战术设计中分别叫“资源库”、“客户端(也可以叫防腐层——ACL)”、“消息发布者”。
从图中可以看出,“南向网关”有“端口”和“适配器”两种角色。简单点来说,“端口”是抽象接口(以 java 为例就是 interface),而“适配器”则是“接口实现”(以 java 为例就是实现了对应 interface 的类)。并且,一般来说“适配器”都是通过依赖注入(DI,控制反转 IoC 的一种)的方式集成到限界上下文的代码中(java spring 中一般是 bean 注入)。
“南向网关”区分“端口”和“适配器”两个角色的好处:一方面是可以让限界上下文内的任何代码都不直接依赖于具体的底层技术细节,如:采用哪种数据库(oracle 还是 mysql、甚至 nosql 数据库)、怎么调用其它上下文服务(本地调用还是远程 RPC)、怎么发布消息通知(本地消息总线、还是消息中间件);另一方面的好处是允许我们随时将构建在一个“微服务”甚至“单体应用”中的多个限界上下文进行拆分到多个进程(即多个“微服务”中),而并不会引起“领域层”、以及“北向网关”的任何代码修改(只要替换并重新打包被依赖注入的“适配器”类即可)。
很明显可以看出,“菱形架构”其实是限界上下文内部所采用的一个“软件架构模式”。而在整个系统范围内,因为包含多个限界上下文,DDD 设计理念并没有要求所有的上下文都严格遵循“菱形架构”——而完全可以根据实际需要(尤其是“基础层”的上下文),视情况而采用其它架构模式(如 MVC 三层架构、大数据计算架构等)。
3系统分层架构图
有了前面的将上下文分为“基础层”和“业务价值层”、以及菱形架构概念的基础,再考虑到“群买菜”系统前端界面针对 3 类用户:商家、客户、平台运营,前两者使用微信小程序,最后一个使用 PC 端,以及相应“伴生系统”的协作边界。我们最后画出系统架构分层设计图如下:
对于上图,需要特别说明的是:
其中腾讯地图、短信系统、微信公众号因为不涉及到业务流程,只是一个功能点,故前面的业务子域、上下文、业务用例都没有展现。
有的上下文有“事件订阅”。这是根据我们前面“限界上下文映射图”中的描述,需要进行消息订阅的上下文才有。
其中比较特殊的一点:“平台集成上下文”没有“领域层”,这是因为它主要完成对微信公众号接口、短信接口、微信开放平台接口的封装,并不存在 DDD 战术设计所需要的“实体”对象这一“领域层”必须的基本要素。
4代码框架结构
基于前面的系统分层架构设计,结合菱形架构模式,我们给出目标系统的如下代码框架结构(采用 java spring 框架开发):
对上图中各目录的划分和命名说明如下:
foundation 目录存放的是“基础层”限界上下文,valueadded 目录存放的是“业务价值层”限界上下文,edge 目录存放的是“边缘层”限界上下文;
本质上,每个限界上下文都可以作为独立的 java 工程存在。但因为本项目由我一个人开发,所以就没有划分工程了;
每个限界上下文,采用“上下文名称+context”命名风格。如:“订单上下文”命名为 ordercontext;
每个限界上下文中,其内部目录结构说明如下:
- north 目录存放的是“北向网关”的内容,包括 local 和 remote 子目录,分别对应北向网关的“本地服务”和“远程服务”。有的上下文的 north 目录下有 subsrciber 子目录,是为了存放消息订阅者代码的。
- south 目录存放的是“南向网关”的内容,包括 port 和 adapter 子目录,分别对应南向网关的“端口”和“适配器”。
- domain 目录存放的是“领域层”内容,也就是核心的业务逻辑所在。
- pl 目录存放的是“发布语言”,其实就是北向网关的“本地服务”向外提供服务时,允许外部调用服务所使用的出入参对象类型。之所以将出入参对象类专门设定一个目录来管理,是因为这些出入参其实是和 domain 目录下的“实体类”是不同的,我们不希望将“领域层”内部的业务逻辑暴露出去(也就是不将“实体类”的业务逻辑暴露出去)。
- 业务价值层有个 sharedcontext,这是因为考虑到代码复用、可能会出现某些“值对象”、“发布语言(即服务接口出入参)”类被多个上下文共用,具体细节我在后面的战术设计中会讲到。
对于“群买菜”系统来说,由于小程序前端界面已经存在,本次是服务端做 DDD 改造,故不打算对前后端交互接口进行调整。为此,我为其设计了“边缘层”,也就是 BFF 层。这就是 edge 目录存放的代码内容。
好了,本节作为“群买菜”系统 DDD 战略设计的第二篇,就先到这儿。下节我们将完成战略设计的最后一步:相关战略技术决策。