在完成了限界上下文的识别(即系统的“粗粒度”模块划分)之后,我们需要分析这些上下文之间的协作关系——即“有界”上下文关系映射”。只有上下文映射完成后,我们才能真正判断我们的“限界上下文识别”是否真的达到了我们想要的“低耦合高内聚”的目标。因为,通过“BoundedContextMapping”,我们可以看到:这些上下文之间的协作关系是什么?这些关系是强是弱?关于“限界上下文识别”和“限界上下文关系映射”,我认为这是DDD战略设计中最重要的部分,甚至可以说:这两项工作将决定微服务切分是否有效的关键因素!但是,肯定有人会说:Boundedcontext没有用到DDD,我凭直觉就能认出来。我的回答是:是的,你似乎可以!但更重要的是,限界上下文的关系映射,将决定微服务拆分后这些微服务之间如何实现“高内聚低耦合”。如果你不使用DDD策略设计,我可以很负责任的告诉你:你以后的“微服务”之间的调用关系肯定会变得不可控!在我实际工作中接触到的一个大型国企的IT系统中,所谓的业务中心几千万行代码部署在十多个微服务中心,80多个%的外设接口调用或者前端接口服务请求都要经过十多个微服务中心!这是真的?可怕的灾难?系统高可用性和业务需求快速响应有什么保证?所以:掌握DDD策略设计几乎是做好微服务设计必不可少的必备条件!不懂DDD也可以做“微服务拆分”未必不如不拆分,单体应用可能更适合你!在这一节中,除了获取限界上下文的映射外,我还将设计系统分层架构,并在最后给出项目组可以直接用于开发的代码框架结构。需要特别说明的是:在写这篇文章的过程中,我反复请教了很多行业专家。一般认为,上述第三条业务子域的分类中,“核心子域”过多。经过深思熟虑,我调整了“核心子域”,这将影响本文系统层次结构的设计。可以跳回第三篇,重新阅读“业务子域识别与分类”部分。短段。在此我再次重申:如果您在阅读中发现我的不足之处,可以随时与我交流。如果我发现不足,我必须认真思考改进。感谢您的支持!Boundedcontextmapping1.Cross-contextusecaseidentificationfor为了识别有界上下文之间的映射关系,我们需要从架构设计的技术角度为跨上下文的业务用例(也称业务服务)绘制服务序列图,然后确定它们之间的映射关系。首先,在第一步中,我们确定所有跨越限界上下文的业务用例。在识别一个业务用例是否越界上下文时,我们必须关注两个基本事实:1)实际上,大多数业务用例都是从“团购”小程序的前端界面发起的,而两者之间的交互前端和服务器不算在内。跨境背景;2)限界上下文提供服务端的业务服务,跨界上下文必须是服务端的逻辑关系。关于如何识别“服务端跨界上下文”的业务逻辑,我觉得有必要对上面列出的所有业务用例一一分析,从以下两个角度进行筛选:初步分析内部业务用例的逻辑,以查看是否需要多个上下文来承担责任。如果是,则需要将该用例纳入分析范围;分析业务用例图中包含的“子用例”,看是否存在分类到其他上下文中的上下文。如果是,则需要将该用例纳入分析范围;为此,识别结果如下图所列:需要注意的是,有几个用例看似是跨上下文的,其实不然。描述如下:“确认购买并付款”、“创建订单预付款”、“完成订单付款”、“接收客户付款”、“退回客户付款”,这些用例实际上是在订单上下文和支付系统之间关系。考虑到我们的系统上下文设计,支付系统其实不在我们的目标系统范围内,所以这里不包含。“后台查询门店产品”、“后台浏览门店订单列表”、“管理门店客户信息”、“管理门店员工”、“开通门店加盟并设置分成政策”、“将品牌门店加入加盟列表”,好像是每一个用例的名称都涉及两个限界上下文,并且与“存储”上下文相关。事实上,他们只需要商店ID,并没有发现与商店上下文有任何业务交互,所以他们实际上不是“跨上下文”。用例来对待。其次,在这些业务用例中,从跨上下文协作的角度来看,其中一些会出现重复的服务时序图,我们需要识别它们:“创建新店铺”、“编辑店铺信息”、使用SMS验证码验证手机号,所以属于跨店和平台融合的两个限界上下文,交互逻辑一致,所以重复。“添加商品到购物车”和“购买Solitaire商品到购物车”其实涉及到订单和商品这两个有界上下文之间相同的交互关系,所以重复。“CreateSolitaireActivity”和“EditSolitaireActivity”,这两个业务用例的操作顺序从跨上下文协作关系的角度来看是完全一样的,只是后者需要加载持久化的内部“Solitaire”上下文信息,前者未被使用。“CreatePaymentOrder”是“ConfirmSolitairePayment”的子用例,所以你只需要保留“ConfirmSolitairePayment”。“将品牌店添加到加盟列表”和“从加盟列表中删除品牌店”,这两个业务用例涉及到店铺与加盟上下文的关系,都获取店铺相关信息和传输店铺信息to加入上下文句柄,所以重复。最终我们确定,绘制服务时序图,进而确定各上下文协作关系的业务用例如下图所示:2.基于跨上下文用例映射上下文关系我们接下来的工作就是将这些业务用例一一映射,进行技术分析,设计服务时序图,然后根据服务时序图确定限界上下文的关系映射。创建新店创建新店涉及手机号短信验证,因此需要跨店跨平台整合两个上下文。服务时序图如下:根据服务时序图,识别“门店”与“平台集成”的上下文关系如图(C表示服务调用客户端,S表示被调用服务端,同上)在DDD标准方法中,一般使用“上层”下游关系”,以及“开发宿主服务OHS”等表达方式,但我认为那是废话,并没有明确表达强弱关系,所以是此处未使用):根据产品UI原型设计初始化店铺默认选项,新店铺创建后,系统机器人需要根据异步事件初始化店铺相关选项:店铺默认门图片(UI允许创建者不设置门图);会调整,所以创建店铺时不设置);店铺第一个员工(即创建者本人);默认加盟店铺分布策略(可能会随着业务的发展而调整,所以在创建店铺集合时不设置);店铺首单位置(详见产品UI原型,StoreSolitaire支持多位置,但首位置为店铺地址);创建商家和商家帐户(如果它是创建者的第一家商店);店铺默认商品分类(系统规则会根据季节调整);店铺默认商品搜索热词(系统后台会根据一定策略不定时更新);这些默认选项涉及“商店”、“员工”、“商品”、“纸牌”、“商家帐户”和“身份验证”6个上下文的协作。服务时序图涉及以下内容:根据服务时序图,我们得到如下图所示的六个限界上下文的协作关系:从上图中的上下文依赖关系,我们可以看出商店有四个上下文对于员工、商品、纸牌、商户账户会产生调用关系依赖,不合理。因为从商业的角度来看,后四种其实都是依赖于门店的存在,而不是反过来。为此,我们做出如下调整:采用消息发布者/订阅者模型,让后4依赖于storecontext发出的“storehasbeencreated”消息;删除在“纸牌”上下文中初始化商店的第一个纸牌位置的逻辑。本质上,我们仔细分析产品UI界面的设计后发现,这其实是“团购小程序”展示创建接龙活动前端界面时调用“门店”上下文服务获取的信息:如果所选择的店铺如果已经有纸牌地址,会返回一个已有的纸牌地址列表供选择;如果商店没有第一个纸牌地址,它将返回商店地址作为默认纸牌地址。修改后的服务时序图如下:根据服务时序图,我们修改context的协作关系如下图(P表示消息发布者,S表示消息订阅者,下同):添加商品到购物车,顾客浏览店铺商品。选择感兴趣的商品到购物车,进行后续的采购结算。考虑到将商品添加到购物车的“时间”特殊性:我们需要在客户将商品添加到购物车时为商品创建一个“快照”,以防止商品信息被后期编辑修改(例如更改图片或描述,尤其是价格),影响对客户的购买承诺。为此,业务用例(服务)涉及“订单”和“货物”两个上下文。服务时序图设计如下:时序图展示了订单和商品的上下文关系如图:发送订单提醒“发送订单提醒”需要在订单上下文中发起,在“通知”,所以涉及到“命令”和“通知”两个上下文。本用例的服务时序图如下:考虑到订单消息提醒没有副作用,不需要保证成功,使用消息发布者/订阅者模型更为合适。即得到“订单”和“通知”上下文的映射关系如下:创建接龙活动类似于“将商品添加到购物车”的用例,商家在创建接龙时也有“时间”纸牌活动和添加产品到纸牌》的特殊性,需要为产品创建一个“快照”,以免后期对产品进行编辑和修改(例如更改图片或描述,尤其是价格),会影响到纸牌活动的开展,为此业务用例(服务)涉及“纸牌”和“商品”两个上下文,服务时序图设计如下:纸牌和商品如图:根据产品UI设计文档浏览我的纸牌,纸牌只能存在于商店下。而“浏览我的纸牌”实际上是“浏览所有的纸牌”所有授权我经营的门店发布的,或者授权门店授权的,或者我参与的品牌门店发布的。”因此,本用例涉及“纸牌”和“商店”两个上下文。服务时序图如下:服务时序图显示,“纸牌”和“商店”这两个上下文其实是不相关的。但是这个服务时序图设计有一种“味道不好”的感觉:让团购小程序的客户端走太多的业务逻辑是不合理的。因此,我们将服务时序图调整如下:服务时序图会引出以下两个上下文的关系:ViewSolitaireDetails》界面发起。在该界面上,客户点击相应的商品加入购物车,或者从购物车中移除,然后点击“我要Solitaire”按钮,进入用例该用例允许用户设置取件方式、取件时间、联系人等信息,然后点击“确认支付”按钮完成支付,点击按钮后,根据商品原型设计,需要完成以下任务:创建订单;更新相关商品的销量,以便后续的商品列表和详情页显示商品销量;初始化ze对应的客户信息,方便商户后续维护客户信息;记录客户参与过接龙,以便客户在“浏览我的接龙”时包含该接龙;根据以上逻辑,我们画出一个服务时序图设计如下:服务时序图中显示的相关限界上下文关系如下:这里可以看到上下文之间有很多调用关系,而且还是有数据一致性的“订单”与“商品”、“订单”与“客户”之间的需求,不利于系统的“松耦合”。为了降低上下文之间的耦合度,我们分析业务需求发现:其实在“创建订单”、“增加产品销量”和“为指定店铺初始化客户”之后,都可以有一定的数据延迟,并且有不需要通过“强制”服务调用关系来保证数据的严格一致性。为此,我们对服务时序图进行如下修改:根据改进后的服务时序图,上下文映射关系可以调整如下下图:结算订单收益结算订单收益分为两步:第一步,客户确认订单完成后,或者机器人超时自动确认订单完成,订单上下文通知商户账户context记录待结算的订单ID(因为订单上下文缺少“判断哪个订单属于待结算状态”的领域知识,所以可以o仅由商家帐户上下文记录);第二步,系统机器人定时触发商户账户上下文,对待结算订单进行收益结算。服务时序图设计如下:第一步的操作可以结合“发送订单提醒”中的“订单完成”领域事件。然而,由于这是一个不可错过的领域事件,这需要“可靠事件模式”。根据服务时序图,识别出“订单”上下文与“商户账户”的关系,如图所示:结算佣金收入结算佣金收入可以与结算订单收入同时执行,也可以异步执行分别在两个用例中。考虑到允许品牌方在affiliatepolicy中设置billingcycle以备日后支持,在两个业务用例中分离异步执行。为此,服务时序图设计如下:时序图展示了商户账号和订单的上下文关系如图:3.有界上下文映射图我们总结分析每个交叉后得到的上下文映射关系-上面的上下文业务用例。最后得到下图:图中实线为服务调用关系,属于“强关系”;虚线是消息通知关系,属于“弱关系”。从限界上下文映射关系可以看出,我们一共有9个限界上下文,其中9个强依赖,9个强依赖链。分别是:接龙靠店铺、靠产品、靠订单,店铺靠平台集成,订单靠产品,商户、员工、客户靠鉴权,商户靠订单。有9个上下文,理论上最多有72条依赖链。经过我们的分析总结,只有11个,已经是不错的设计了。基于这样的综合评价,我们认为目前的设计已经达到了“可以接受的程度”,我们暂时不会做更多的优化和调整,避免“过度设计”。系统架构及代码框架1.业务子域与上下文的映射完成限界上下文的关系映射,实际上是对整个系统架构进行分层设计的基础。从分层的角度来看,系统的整体架构设计包括:边缘层、业务价值层、基础层。边缘层一般是前端UI的各种控制器。业务价值层和基础层包含所有有界上下文。基础层包含支持子域和通用子域对应的限界上下文,而核心子域对应作为业务价值层的限界上下文。为了区分业务价值层和基础层,我们先将前面得到的业务子域(见第3章全局分析内容)与限界上下文的关系映射如下表:Whatneeds需要说明的是:其中“商户管理”和“店铺管理”虽然是支持子域,但是由于分别合并到“商户账户”和“店铺”的上下文中进行开发实现,逻辑代码实现为也分为“业务价值层”。二、菱形对称架构介绍在对整个系统进行“分层架构设计”之前,我们需要熟悉一种软件架构模式——菱形对称架构(以下简称菱形架构),如下图所示:对于“RhombicArchitecture”的描述如下:菱形架构的基础取决于定义“领域层”。这就涉及到了后面要进行的DDD战术设计内容,这里就不展开了。你只需要知道:“领域层”包含了业务领域中关键的业务知识,包括“聚合(包括实体对象、值对象)”和“领域服务”。其实我们追求的“高内聚”主要体现在“领域层”。业务需求变化引起的变化,我也希望能够实现“领域层”的“聚合”和“领域服务”的变化。完成。因此,“领域层”是DDD的“核心代码位置”。我们希望将所有不在“领域逻辑”层的代码封装到“北向网关”或“南向网关”中。具体描述如下:“北向网关”实际上是上下文对外提供服务或接收消息通知的入口。如果上下文只需要提供同进程内部的应用服务调用接口,或者接受消息通知(通过消息总线),那么就需要“本地”服务;如果上下文需要是一个独立的进程(此时一般是云原生的独立微服务)来导出服务,或者接受消息通知(通过消息中间件),就需要将“本地服务”封装为“远程”服务。也就是说:北向网关中的“本地服务”和“远程服务”是一一对应的,“远程服务”只负责转换输入的格式,“负责调用”领域层的“聚合”或“领域服务”来完成具体的业务逻辑(我将在本主题后面的章节中演示“聚合”和“领域服务”的内容)。“南向网关”实际上是上下文用来从业务逻辑中“剥离”这三类技术细节的主要手段(图中只画了第一种):访问底层数据库,调用其他上下文服务,以及向其他上下文发送数据发布消息通知。这三个技术细节的封装在DDD战术设计中被称为“资源库”、“客户端(也叫防腐层-ACL)”和“消息发布者”。从图中可以看出,“南向网关”有两个角色:“端口”和“适配器”。简单来说,“端口”就是一个抽象接口(比如在java中就是interface),“adapter”就是“接口实现”(比如在java中就是实现了对应接口的类).而且,一般来说,“适配器”是通过依赖注入(DI,一种控制反转IoC)的方式集成到限界上下文(一般是javaspring中的bean注入)的代码中。“SouthboundGateway”的优点是区分了“端口”和“适配器”两种角色:一方面,它允许限界上下文中的任何代码不直接依赖于具体的底层技术细节,例如:哪个数据库(oracle或者mysql,甚至nosql数据库),如何调用其他上下文服务(本地调用或者远程RPC),如何发布消息通知(本地消息总线,或者消息中间件);另一个优点是它允许我们在“单体应用”中构建一个“微服务”甚至多个限界上下文被拆分成多个进程(即多个“微服务”),而不会在“领域层”中引起任何代码和“北向网关”修改(只需替换并重新打包依赖注入的“适配器”类)。很明显,“钻石架构”实际上是一种在限界上下文内部使用的“软件架构模式”。在整个系统中,由于包含多个限界上下文,DDD设计理念并不要求所有上下文都严格遵循“钻石架构”——而是可以根据实际需要(尤其是“基础层”的上下文)。视情况采用其他架构模式(如MVC三层架构、大数据计算架构等)。3.系统分层架构图基于之前将上下文划分为“基础层”和“业务价值层”,以及菱形架构概念的基础,考虑到“团购”系统针对三类用户:商家、客户、平台运营,前两种使用微信小程序,最后一种使用PC端,以及对应的“随行系统”的协作边界。最后画出系统架构层次设计图如下:对于上图,需要说明的是:其中腾讯地图、短信系统、微信公众号只是一个功能点,因为它们不涉及业务流程,所以之前的业务子域、上下文、业务用例都没有展示出来。一些上下文有“事件订阅”。这个根据我们之前的《限界上下文映射》中的描述,只有需要订阅消息的上下文。特殊点之一:“平台集成上下文”没有“领域层”,因为它主要完成微信公众号接口、短信接口、微信开放平台接口的封装,没有“领域层”DDD战术设计所要求的。实体”对象,“领域层”必须是基本元素。4.代码框架结构基于前面的系统分层架构设计,结合菱形架构模式,我们给出目标系统(使用javaspring框架开发)的代码框架结构如下:上图分别为:foundation目录存放“基础层”限界上下文,valueadded目录存放“业务价值层”限界上下文,edge目录存放“边缘层”限界上下文;本质上,每个限界上下文都可以作为一个独立的java工程存在。但是因为这个项目是我一个人开发的,所以没有分项目;每个限界上下文都采用“上下文名称+上下文”的命名方式。例如:“订单上下文”被命名为ordercontext;在每个限界上下文中,其内部目录结构描述如下:north目录存放“北向网关”的内容,包括本地和远程子目录,分别对应北向网关的“本地服务”。”和“远程服务”。有些context在north目录下有一个subrciber子目录,用来存放消息订阅者代码。south目录存放“南网关”的内容,包括port和adapter子目录,分别对应南网关的“port”和“adapter”。域目录存储“域层”内容,这是核心业务逻辑所在的地方。pl目录存放的是“发布语言”,其实就是北向网关的“本地服务”对外提供服务所使用的输入输出参数的类型。之所以专门设置一个目录给进出对象类来管理,是因为这些进出对象其实和域目录中的“实体类”是不一样的,我们不想暴露里面的业务逻辑“领域层”(即不暴露“实体类”的业务逻辑)。业务价值层具有共享上下文。这是因为考虑到代码复用,一些“值对象”和“发布语言(即服务接口输入输出参数)”类可能会被多个上下文共享。稍后我会描述细节。在战术设计中会提到。对于“团购”系统,由于小程序的前端接口已经存在,本次服务端在做DDD改造,所以不打算调整前后端交互接口。为此,我设计了“边缘层”,也就是BFF层。这是存放在edge目录下的代码内容。
