DDD 实战 (4):从需求到代码实现生鲜电商系统
我们前面已经完成了“群买菜”在“问题空间”的全局分析,本节开始进入“解空间”映射。我将用两节的篇幅来讲解“群买菜”的战略设计。首先,对战略设计的理论知识做一个浓缩性介绍;其次,分两节介绍“群买菜”的 DDD 战略设计,包括:本节介绍系统上下文定义、限界上下文识别;下节介绍限界上下文映射、系统分层架构、战略层技术决策。
1. 战略设计工作内容
在上节中,我提到“解空间”映射包含 3 部分的内容:战略设计、战术设计、代码实现。其中,战略设计主要包含 3 部分工作:
系统上下文定义
限界上下文识别及其间关系的上下文映射。
战略层面的技术决策。
下面我就这 3 方面的工作先做个进一步的介绍(这种在实例演示之前,同步的简要介绍相关的 DDD 理论概念,是本专题的写作风格之一):
系统上下文识别。所谓“系统上下文”其实在满足业务目标的前提下搞清楚两个问题:1)目标系统还需要和哪些伴生系统合作(也就是第三方系统、遗留系统等);2)目标系统和这些伴生系统是怎么进行职责分工的;
限界上下文识别。“限界上下文识别”就是目标系统内部最粗粒度的模块划分,而要识别出“限界上下文”,就要在遵循“单一职责原则”的底层逻辑基础上,对前面全局分析阶段得到的“业务用例”进行知识语境角度的归纳分类(考虑语义、功能相关性、亲密度等因素),实现对业务能力的“纵向切分”。并在完成切分后,综合考虑开发团队管理因素、技术因素后,用 3 个验证对切分结果进行验证和调优(正交原则、单一抽象层次原则、奥卡姆剃刀原则)。本节中我会演示我是怎么进行“群买菜”限界上下文识别的。
限界上下文关系映射。“限界上下文关系映射”就是搞清楚这些“限界上下文”模块之间是怎样的协作关系。进行“限界上下文关系映射”的最好方法,就是从前面全局分析得到的“业务用例”中,挑选出跨限界上下文的那些“业务用例”,然后用“服务序列图”的方式对这些用例服务的实现过程进行分析,最后归纳找出合适的“限界上下文关系映射”。同样,我会在下节中演示怎么进行“群买菜”限界上下文映射的。
战略层面的技术决策。一般来说可能涉及到的战略层面的技术决策,我这里给出 8 个重要方面(“群买菜”的这部分设计在下节介绍):
各个限界上下文技术栈的选择。理论上,极端一点我们甚至是可以在每个限界上下文内使用不同的技术栈的,甚至不同的开发语言。以最大可能的实现“限界上下文”之间的“松耦合”。
规则引擎考量。是否引入规则引擎,以及将规则引擎用在哪个限界上下文内部、或新增“共享内核”上下文来实现“规则的统一管理和运行”。
CQRS 模式考量。为了避免过多负责的查询逻辑扰乱了“领域模型”的稳定,需要考虑是否引入 CQRS(查询命令职责分离,具体含义见后面的“群买菜”战略技术决策示例)模式的软件架构、以及在哪些限界上下文引入 CQRS 模式。
命令总线模式考量。对于需要快速响应前端请求、而又可忽略对前端用户的“即时信息反馈”的情况下,考虑是否引入后台“异步执行命令”的模式——先提交请求、再后台进程异步完成处理。
事件溯源模式或事件总线考量。对于大量事件消息交互的情况下,考虑是否引入事件溯源模式的软件架构、或只是简单的引入事件总线(如消息中间件)。
微服务划分考量。根据“限界上下文”的识别、以及它们之间的映射关系,结合下面将提到的事务一致性和数据库架构,确定如何划分“微服务”。需要特别指出得是:采用 DDD 设计,不见得非要实现“微服务应用部署”,当没有确切把握时完全可以将多个“限界上下文”放在一个微服务内部、甚至整个系统实现“单体应用部署”——单体应用部署并不是问题的核心,问题的核心是“模块的高耦合、低内聚”。
事务一致性策略和数据库架构。结合“限界上下文”的划分边界、以及它们之间的映射关系,以及项目建设的实际投资和资源情况,考虑数据库架构(集中单体库、分库分表等),并同时考虑“数据一致性”采用怎样的策略:全局事务一致性、最终事务一致性还是别的策略。
缓存技术考量。在很多大型系统中,为了确保响应性能,可能会考虑引入缓存技术(如:redis、memcache 等)。如果引入缓存技术,从高可用角度考虑,就需要更进一步确定两个关键技术决策:是否要满足 cache miss(也就是在缓存查不到时是否到数据库查询)?如何确保缓存数据的及时刷新?
系统分层架构设计。除了上面提到的“系统上下文定义”、“限界上下文识别和关系映射”、“战略层面技术决策”3 个最重要的工作外,其实还有个可选项:系统分层架构设计。它是为了解决 3 个问题(“群买菜”的这部分设计在下节介绍):
系统是否需要边缘层(BFF 层,面向前端的后端),如果需要采用什么技术栈实现;
将“业务子域”与“限界上下文”进行对照关系映射(如果没有遗留系统,应该是满足“同构”映射的);
根据“业务子域”的分类将情况,对限界上下文区分出“基础层”(一般含“支撑子域”和“通用子域”对应的限界上下文)和“业务价值层”(一般放“核心子域”对应的限界上下文)。
2. “群买菜”系统上下文定义
本系统的主要用户是 4 类:消费者客户、商家创建人、商家授权操作人、平台运营人员。本系统前端是运行在微信环境里的小程序,会使用到微信支付系统执行相应的支付功能、会使用微信公众平台发送消息提醒、会使用短信平台发送验证码和消息提醒、会使用腾讯云存储上传图片文件、会使用腾讯地图帮助用户定位。因此“群买菜”系统上下文如下:
3. “群买菜”限界上下文识别
根据张逸老师 DDDUP 的建议,限界上下文的识别,主要采用如下图所示的 V 型映射工作法:
在前面全局分析章节,我们已经完成了 V 型映射的前半部分:从业务流程到业务服务(即业务用例)。本节战略设计部分,主要需要完成 V 型映射的后半部分:从业务服务(业务用例)到限界上下文。我们将通过如下的 4 个步骤来得出最终的限界上下文划分及其映射关系:
1.首先,我们从语义相关和功能相关两个角度对业务用例进行归类和归纳,得到业务主体,作为限界上下文候选项;
2.其次,我们按照这些领域概念的亲密度和知识语境对归类归纳后的业务主体进行调整,作为限界上下文;
3.再次,我们还要基于如下 3 个原则对限界上下文进行检验,并在适当时进行调整。
原则 1:正交原则。包括业务能力正交、领域知识正交、领域模型正交。
原则 2:单一抽象层次原则(SLAP),即永远确保在同一层次上进行抽象。
原则 3:奥卡姆剃刀原则。如无必要,勿增实体。
4.最后,我们根据系统上下文的边界、以及其它的一些技术实现因素,对限界上下文进行适配。因为限界上下文最终一定是解空间的设计方案,所以这是一个跳不过去的过程。
在按照以上 4 个步骤分析的过程中,我们需要注意的是:我们要始终考虑限界上下文的主要 4 个设计特征:最小完备、自我履行、稳定空间、独立进化。简单点说,就是单一职责原则:不应该有两个以上因素引起某个限界上下文发生变化。
3.1 基于语义和功能相关识别上下文
下面,我们先来进行第一步,即根据语义和功能相关对业务用例进行归类和归纳,得到业务主体,作为限界上下文候选项。下图是我们做的第一步归类归纳后的业务主体识别结果:
其中,有 6 个业务用例我们暂时没有为其找到合适的业务主体归属:加商品到购物车、选购接龙商品到购物车、确认订单付款、确认接龙付款、管理客户信息、创建商家及账户。关于它们的说明如下:
“加商品到购物车”、“选购接龙商品到购物车”两个用例是因为难以抉择是否考虑增加“购物车”限界上下文来单独存放其归类(一般来说,“购物车”应该属于“订单”上下文的内容,但这里因为有了与“订单”并列的“接龙”上下文而显得比较微妙);
“确认订单付款”、“确认接龙付款”两个用例很难归类的原因是难以判断其属于订单/接龙、还是账户;
“管理客户信息”用例看起来是个单独的业务主体“客户”,但因为只有一个用例故有点微妙;
“创建商家及账户”看起来应该有个单独的业务主体“商家”,但同时好像也可以归类到“账户”业务主体下;
3.2 根据业务主体亲密度和知识语境调整
接下来,我们继续进行第二步的分析:根据业务主体之间的亲密度、知识语境进一步调整,找到限界上下文。在上一步的工作中,我们遗留下 6 个业务用例难以归类。下面我们来一一进行分析:
1. 其中“加商品到购物车”和“选购接龙商品到购物车”是因为按照一般电商系统的惯例,“购物车”应该归到“订单”上下文中,但考虑到现在有“订单”和“接龙”两个上下文,那这两个跟购物车有关的用例,到底应该归属“订单”还是“接龙”?还是两个用例分别归属一个上下文呢?实际上,我们仔细分析后,可以发现:其实“接龙”只是一种商品打包、且预定式销售方式,是一种“营销方式”,并不能和“订单”上下文在业务价值上同等对待。而“订单”上下文更多是各种营销方式下产生销售后的一系列行为。就好比,如果本目标系统还有“秒杀”、“团购”、“砍一刀”等营销方式,这些营销方式并不能和“订单”上下文并列,反而应该是“订单”上下文的调用方——因为每种营销产生的订单,都最终要使用“订单”上下文进行订单管理。所以,“购物车”这一领域概念从亲密度上来说,怎么也应该归属“订单”上下文。为此,我们做这样的处理:
a) 将“加商品到购物车”和“选购接龙商品到购物车”作为“订单”上下文的内容;
b) 并预计在将来的战术设计中,会出现“购物车”这一聚合根实体对象;
2. “确认订单付款”、“确认接龙付款”这两个业务用例,我们深入分析其业务逻辑,事实上是在客户选定待购买商品、并放置到购物车后,在购物车的基础上做的操作。所以,其实这两个业务用例,可以合并为一个业务用例叫“确定购买并付款”。考虑到该用例与购物车的亲密度、以及只有“购物车”概念才具备其购买和付款所需的全部领域知识(“支付”上下文所需要的支付金额、以及商品列表和描述等,都来自于“购物车”),我们就应该将这个业务用例放到“订单”上下文去。
3. 关于“管理客户信息”用例,因为实在无法跟任何已有的业务主体合并归类,故应该大胆的就设立一个只有一个业务用例的业务主体“客户”。
4. 关于“创建商家”业务用例,我们继续留到下一步分析。
经过第二步的分析,我们调整限界上下文识别如下图:
3.3 用 3 个原则对限界上下文验证
我们再进行第三步的分析:基于如下 3 个原则对限界上下文进行检验,并在适当时进行调整。包括:正交原则、SLAP 原则、奥卡姆剃刀原则。同时,我们在这一步,再回顾限界上下文设计的 4 个特征:最小完备、自我履行、稳定空间、独立进化。下面是分析过程:
1. “创建商家”这个用例看起来貌似应该有个业务主体“商家”、也貌似可以归类到业务主体“账户”。我们分别考虑两种方案:
a) 我们首先考虑方案 1:新增业务主体“商家”。如果我们这么做,就存在违背 SLAP 原则(单一抽象层次原则)的风险。因为“商家”业务主体,看起来也应该包含“加盟”这个限界上下文,这就必须将“商家”和“加盟”合并后,成为新的限界上下文“商家与加盟”。但其实,这是有问题的。因为,目标系统里面的“加盟”其实是“店铺”之间的加盟关系,这考虑的话,“加盟”就应该和“店铺”、“商家”这 3 个上下文合并起来统一叫“商家”。但是这样的话,就又上下文设计的违背了单一职责原则:这一个上下文会可能受到 3 个独立影响因素的变化而变化:既可能在将来因为平台方对“商家运营”规则的调整而变化,也可能因为加盟本身业务规则的变化而变化,还可能因为店铺管理的业务规则变化而变化。
b) 其次我们考虑方案 2:将“创建商家”放到“账户”上下文。我们是根据商家创建人来创建“商家”并进而创建“商家账户”的,并且“商家账户”一定是一对一绑定到“商家”的,从亲密度上来看,确实“商家”和“账户”的关系更为亲密。但另一方面,将“商家”合并到“账户”上下文,却又存在违背单一职责的风险。
c) 事实上,从目前“商家”需要实现的业务行为来看,只有一个“创建商家”行为,并没有太多的其它领域知识需求。从这个角度来说,我们将其暂时放在“账户”上下文一起,是可以接受的。况且,如果按照奥卡姆剃刀原则,我们在难以抉择一个业务主体要不要独立上下文时,就先不独立。所以,从总体上来说,我们倾向于选择方案 2:将“商家”合并到“账户”上下文,合称为“商家和账户”。
d) 同时,事实上“创建商家”和“创建商家账户”在目前目标系统的实现中,因为业务知识很单一,它们总是同时出现的,所以我们干脆将“创建商家”和“创建商家账户”合并为“创建商家和账户”一个业务用例。
2. 其次,我们再来看“发送短信验证码”用例,因为涉及到跟伴生系统短信平台的接口,这个用例看起来可以放到一个独立上下文中,也可以作为具体调用该用例的上下文(目前只有店铺)的防腐层 ACL 存在。但事实上,我们再次通览到目前为止的上下文识别、及其内包含的业务用例,还发现一个问题:“发送订单提醒”属于“订单”上下文,但实际上这个业务行为的变化因素,很可能是我们采用提醒的技术设施扩展、以及技术设施本身的业务规则变化而引起的变化,这和“订单”上下文本身会因为订单管理规则变化而变化,其实是两个不同的影响因素,违背了“单一职责原则”。因此,我们需要将其独立出一个新的出来。所以,“发送短信验证码”、“发送订单提醒”可以考虑放到一个“消息集成”上下文去。
3. 再次,我们再来看业务用例“获取微信绑定手机号”。这个用例看起来跟“发送短信验证码”类似,可以作为某上下文的 ACL 存在、也可以做为独立上下文存在。如果作为 ACL,就因为有“订单”和“店铺”两个上下文都用到,会出现重复实现的问题。因此,优先选择作为独立上下文。但是其做为独立上下文又过于孤立,因此综合考虑后,可以和上段提到的“消息集成”上下文合并,取名为“平台集成”上下文。从其职责上来说,“平台集成”这个名称也是合理的,因为其中涉及到的内容,全部跟微信平台有关(公众号信息、微信绑定手机号、短信平台也可以用腾讯云自带的短信 SDK)。这还暗示着:如果把目标系统移植到支付宝小程序、抖音小程序等,只需要调整“平台集成”上下文的实现即可。
4. 最后,我们看“加盟”这一上下文,其实从业务角度来说应该是和“店铺”分开的。但鉴于现在系统提供的加盟管理功能很弱,只是加盟政策和店铺之间加盟关系的管理。这就会导致一个问题:到底是“加盟”独立出来一个上下文呢?还是和“店铺”上下文合并为一个上下文?根据奥卡姆剃刀原则,当我们难以抉择是否需要独立限界上下文时,我们就执行“如无必要、勿增实体”原则。为此,对于这种犹豫纠结的情况,我们就将“加盟”和“店铺”上下文合并为一个。另外,这两个上下文合并后,我们还发现业务用例“搜索品牌店铺”其实是“添加品牌店铺到加盟列表”用例的一个步骤,因此我们将这两个业务用例也合并。
经过第三步的分析,我们调整后的限界上下文列表如下图:
3.4 考虑技术和管理因素
我们还有最后一步分析:根据系统上下文边界、以及技术实现因素,对限界上下文的识别情况进行最终的确认。从本目标系统的系统上下文情况来看,目前技术因素我们能看出来,如下 3 方面需要考虑的因素:
1. “支付”上下文其实就只能是微信支付提供的能力,这个上下文不属于我们目标系统的边界,而属于伴生系统,故应该删除。
2. 我们还使用“腾讯地图”这一伴生系统对用户的手机位置进行定位,但该功能目前主要用于两个地方:
a) 用于“店铺”上下文中帮助用户自动定位最近店铺;
b) 根据产品经理的 UI 设计,在“查看店铺详情”页面上需要为用户提供地图导航功能,让客户能够找到商家的线下店铺;
c) 在可预期的将来,也不会有更多的上下文会使用腾讯地图功能。为此,就不设立独立的、可作为统一对接上下游系统的“定位导航”上下文了。
3. 另外,按照常规系统建设需要,在其它一些系统建设中,诸如报表统计模块可能会引起 ETL/大数据之类的上下文识别、以及秒杀等特殊性能并发要求引起的特定上下文等,是需要考虑进来的。但我们这个目标系统目前比较简单,没有涉及到后台的数据分析统计、也因为是产品早期阶段不考虑“秒杀”等特殊性能要求的业务场景,故不做这方面的设计。
在真实的大型项目中,还要考虑项目团队的能力和边界问题——“康威定律”,你不应该让一个“限界上下文”被拆分到多个开发团队去负责、而只能让一个团队负责多个“限界上下文”。由于“群买菜”系统完全有本人独立开发,所以这里就不相关了。
最终,我们的限界上下文识别结果如下图(为了更清晰的表达,已省去上下文内部的业务用例):
本节文章就到这里了,下节文章中,我将完成“战略设计”中剩下的工作:限界上下文关系映射、系统分层架构设计、战略层技术决策。
版权声明: 本文为 InfoQ 作者【深清秋】的原创文章。
原文链接:【http://xie.infoq.cn/article/5ac7de3774f38146010c67089】。文章转载请联系作者。
评论 (5 条评论)