在DDD讨论群中与一位群友讨论了一个关于领域服务拆分的问题,这个也涉及到了代码层面的操作和设计,比如一个领域服务中包含多个子领域,随着业务的发展或者迭代,某个子领域需要拆出来独立迭代。很多程序员多少都会遇到这种情况,尤其是分布式微服务下的架构模式,因此本文就这个话题着重讨论一下DDD中的领域上下文的拆分和合并。
在讨论这个话题之前,先回顾一下关于这个话题的相关DDD模式:抽象核心模式,分离核心模式,共享内核模式,通用子领域模式,持续集成模式,演进顺序模式。相关度比较高的就是分离核心模式了。当然像演进顺序模式和持续集成模式跟这个话题虽然没有直接的关系,但是在领域拆分和合并的过程中会发生很多事情,比如领域上下文随着子域的拆分和合并会发生变化,进而会带着领域服务和接口的变化,同时会产生一系列问题,比如数据迁移,接口兼容等。
领域拆分场景其实很常见,不仅关于领域拆分也关于服务拆分,数据库表拆分,当然还有架构拆分等。拆分的目的各有不同。以下是一些常见的拆分场景:
当然,如果有其他领域或者服务拆分的场景也可以一起讨论哈。下面我们通过两个不同行业的场景来描述一下领域拆分会是怎么样的,以及背后有哪些工作来帮助领域顺利拆分。
首先我们看一下电商类的商品中心,由于我部门最近在梳理商品和货品中心,所以对这块略有了解。我最近几个月的工作是负责交易中心的迭代,由于公司业务特性交易这边跟商品和货品并没有太大关系。总体上我最近2年左右的时间都在电商领域里积累知识,现在我们假设要建设一家电商公司,需要建立一个以垂直类商品为核心的商品交易中心。以下是我画的一个领域上下文图。
从单体服务上看,假设建设一个可以支撑日单量2000左右的商品交易中心的话,其实这个就是进销存系统的翻版。那假设我们现在要拆分了,老板说公司有融资了可以跟阿里这样的电商公司battle了。拉新,做广告,搞促销,从垂直类电商进军到平台类电商。作为程序员也跟着忙活了,招人,改系统…
假设现在有5w左右的用户,第一期拉新到起码有200w用户,日单量20000的量级。由于人手和排期问题不可能全部把商品中心的子领域全部拆出来,那么先拆哪一个呢,假设按优先级先拆订单子领域。那么现在就会遇到第一个领域拆分的场景:单体系统架构的应用转为微服务架构。
如果有项目负责人或者架构师肯定会做一些工作来保障拆分的时候不会停服,那列一下大概有哪些工作要做:
当订单交易中心独立完成之后,下一步继续拆分,商品中心这个核心子领域肯定不能继续在这个单体服务中了。所以第二期继续对商品中心的核心子领域动手,由于是核心领域所以不能像订单交易子领域那样直接拆出来,所以先把搜索上下文提取出来,当作独立的商品中心子领域,搜索服务单独维护,搜索功能逐步从老商品中心关闭,那么就会遇到第三种领域拆分的场景:从子领域模块拆分到领域服务。对于搜索来说拆分过程应该不会太难,那下面继续拆分。
搜索拆分出来之后,根据规划进一步建设新商品中心,单独维护商品的sku,spu,商品详情,搜索等业务。商品详情在c端优先支持的情况下,需求优先级比较高,那么商品详情上下文也会显示出来。这就会遇到第二种领域拆分情况:从核心领域服务中拆分到子领域模块。商品详情可以放在老商品中心作为临时方案,同时与新商品中心的详情页进行对接。由老商品详情页作为兜底等。
商品中心独立的过程肯定会涉及到权限,日志,运营管理,所以这些迭代路线也会跟4,5,6这三个领域拆分场景相关,就不一一介绍了。
最后,介绍一下相对恶心或者混乱的两个场景,对于6,7这两个场景肯定是在很多领域服务已经落地且上线的时候发生的,比如上面的商品中心,当商品中心全部功能下线之后,新的局面肯定是有很多领域服务和工程一起支撑公司业务。在某些领域或者上下文中的迭代过程并不十分完美,总有新系统新功能去补上来,那么对于现有领域服务来说只有两种结果一种是半死不活,另一种就是下线。所以到这个地步的时候从全局看所有领域服务和工程就会觉得这么多工程肯定有些是重复的,有些是冗余的,有些拆的太细了,需要合并。
下面看一下企业办公类的场景,我曾经在北京一家公司呆过三年多,并且一直在企业办公领域深耕。对于上面第6,7类领域拆分的场景深有感触。当微服务和分布式系统去支撑业务的时候,服务拆分粒度和维护就变得异常困难。新服务,老服务如何协调,从领域上下文的角度来看,某个领域服务或者某个单独的微服务进程或者工程实际上算是子模块,或者上下文。只是没有聚合到一个大型工程下而已,所以有很多git地址和仓库。之前发的文章中介绍过企业服务架构演进相关的主题,这里我简化一下。从领域上下文图的视角梳理一下。
上图还有一些没有列出的上下文或者子领域,比如会议室预定等。上面这些每个子领域都起码有2个工程在服务。很多时候,微服务拆分之后我们对于领域服务能力的把控看似变好了,实际上当服务数量和接口变多之后这些并没有太多改善。尤其是新的服务诞生之后领域内上下文和服务的协调就有点别扭。比如员工核心领域,企业办公的核心领域就是员工,针对员工做了很多事情,包括员工hr系统,对外接口工程,与用户子领域共用一个库,模型也基本共用等。这些在企业办公迭代的过程中随着业务需求的变化或者人员的变化就会带来领域的拆分,独立或者下线。
领域合并的场景其实很少见,如果在技术开发领域里呆上几年肯定会遇到这种情况,比如老系统下线,不同项目工程重叠,大型项目重构会伴随着部分领域的合并,新领域的诞生等。在领域合并的背后也包含着一些动力,比如架构设计的前瞻性会提前梳理领域服务。领域合并的时候很多代码都将成为死代码或者僵尸代码,因为维护的人宁愿不动也不会找麻烦。这里我也简单列一些领域合并的场景,如果你遇到其他场景也欢迎跟我一起讨论哈。那下面看一下有哪些场景:
其实在电商类的订单中心中这块我理解起来比商品中心难一些,因为从订单中心的视角去看的话一方面订单要有状态控制另外一方面订单要链接用户,商品,优惠,支付,退款这些,更复杂的会有交易的参与。比如有些订单并没有支付,有些订单没有退款等特性。所以从领域上下文上划分订单中心会比较复杂,因而不同场景下的订单中心都几乎不太一样。下面假设以订单中心为核心领域的话其子域和上下文应该是怎么样的,请看下图:
假设现在你在一家电商类公司,由于业务高速发展,初期订单领域由不同业务方自己建设,或多或少有相对独立的服务和数据库支撑业务。现在公司下决心参考阿里等中台建设大拿的方法论来建设自己的中台。现在要做的一件事就是要把订单中心中订单整个流程的管理收口到中台项目的订单中心里。那如果你是项目经理或者你是项目的参与者,你可能会需要做以下事项:
上面的臆想其实跟第3,5种情况非常相似。单体项目的领域合并这里就不再讨论了,其要关注的点和相关细节会在第五小节讨论。下面我们继续微服务场景下的领域合并话题,假如这家电商公司中期发展不错,同时融资了,且收购了同类型电商公司。被收购的公司也有技术开发人员和订单中心,现在为了在电商领域大展身手需要融合被收购公司的订单,商品,运营团队和服务,提高单量和效率。这种不同公司相同业务的领域服务合并就类似于第6种情况。
更常见的一种情况就是上面这类的电商公司一般也会跟天猫,抖音,拼多多的大平台进行合作,从而获取订单流量,那么自己的技术开发团队负责的业务领域和服务必然会跟大厂的业务领域服务做适配,这种严格意义上说并不是合并,而是关联或者适配。只不过对于大厂的对接站在中小电商公司来说领域适配关联其实并不太容易。
在权限领域或者erp,crm系统中权限几乎是通用的支撑类的领域服务平台,很多技术开发或多或少都参与过权限类的功能系统开发。关于权限能力服务的演变不同的公司或者开发人员都有一部血泪史,因为权限模型或者权限功能在不同的公司其需求和特性真的非常不一样。
在权限功能模块或者系统里不管是单体服务还是微服务都将出现领域拆分和合并的场景,我在北京的一家公司工作过3年多,做过4版权限系统,权限系统有多个版本同时在线,同时像招聘,hr系统都还有一些自己业务的权限管理功能,并没有完全收口到权限系统。这里探讨权限系统的合并场景其实跟上面的差不多。所以不再深入,如果感兴趣的话可以一起讨论。
经过上面的讨论我们可以知道领域拆分和合并是常有的事情,在软件系统中没有什么是永恒不变的,尤其是互联网领域,很多系统和服务都会被开发和产品玩来玩去。俗话说从一个坑跳进另一个坑,就是这个道理,即使不从领域服务或者DDD的视角看问题,但是本质是一样的。下面看一下如何面对领域或者服务拆分所产生的问题。
在代码层面上的变化主要发生在业务和模型的变化,比如拆分的时候业务和模型可能会复制到不同的工程里面,合并的时候遗留的模型和方法接口没有被删掉。从0-1建设的项目还好,如果是已经建设的或者接手维护迭代的这种,领域拆分之后经常不会删除代码。这就很容易造成问题。
最好的方式是添加注释和文档,或者标示过期,以及给出对应的模型,数据库表变化版本,每个版本都有不同的变化内容说明。举个例子,某个系统需要将权限功能迁移或者当前的系统需要接入新的权限系统,那么对于权限相关的功能,模型,数据库表这些都可以作为备份并随着下一次迭代删掉。
另外一种形式的情况就是当前这个领域服务的子域或者上下文需要迁出独立服务,但是有上下游关系,那么就需要做一些适配的工作,这里的代码删除就有点讲究了,比如领域内的bo,dto,vo这些都将要删掉,相关业务接口和方法都要迁出来,然后可以加一些适配代码。如果不能一步到位那么可以微调一下,就是将这些需要迁出的子领域或者上下文单独列一个包,来标示一下,同时相关业务代码的上下文可以继续存在,只是交互要跟新领域服务做交互,后续慢慢将整个需要迁出的领域上下文相关的代码删掉。
在架构设计层面涉及到的东西其实比较多了,比如job,api这类的工程通常会调用核心服务,但是也有一些情况是这些工程会独立访问数据库,自己做entity,bo,dto这些,这对整个工程设计和业务迭代都有很大影响,毕竟加个字段很多地方都要改。
在这一层面上涉及到领域拆分和合并的时候首要考虑的是稳定性和可用性,比如数据库的迁移,服务上下线,功能开关,流量路由等,出现问题可以有兜底方案。其次考虑业务上下游调用的模型迭代,由于领域拆分和合并肯定涉及到代码改动,那么领域模型和服务都要更新,需要把不是自己业务领域内的对象全部迁移走。
领域拆分和合并之后,上下游协作关系和沟通成本都会有变化,这些变化也体现在调用链路上,从模型上不太能看出来这些架构上的调整,只有经历过这些的相关人员才会了解架构设计或者重构前后的变化。架构设计要帮助上下游领域服务划清边界,明确职责,所以在设计文档和方案中不仅要明确技术方向的变化,也要明确业务模型和服务职责上的变化。这一点在DDD模式上有一些体现,比如上下文图模式,演进顺序模式和突出核心模式。
最后一个就是架构和工程设计评审,由于设计到工程服务的划分,上下线,功能迭代,维护什么的,不同的工程风格不统一,命名也不统一,就会让人很头疼。举个例子,dubbo工程,经典的可以是一个父级模块,带两个子模块。也可以是一个父级模块带三个,四个。然后这些每个子模块的命名都不够通用化,不能一看就懂。里面的分包策略就更不用说了。所以在进行领域拆分和合并的时候这些命名上的规范,模块上的设计都非常重要。风格上的统一可以帮助更好的维护系统。
接口和文档层面在领域拆分和合并的时候也经常出现问题,比如接口拆分之后接口没有下线,代码没有删除或者标示过期。接口文档没有跟着更新。接口和接口文档随着部门和业务的变化从文档仓库频繁迁移等。从领域的角度来看,模型的变化很难在文档中体现,这就让维护和迭代的人对一个新系统来说增加理解成本。最好的方式就是领域服务文档不跟着部门文档走,这些领域服务文档既包括接口文档,也包括数据库文档和领域分析文档(实体,值对象,服务接口,模型上下游关系)。这样的话,不管部门名称,职能怎么变化,负责的商品或者订单中心永远都在这个文档仓库里。只是当前负责维护的人是当前这个部门的人。
在实际开发中很少有人这么做,很多团队基本上都会自己维护一套,组织架构,业务形态稍微变化,文档就跟着混乱起来。有复制文档的,有迁移文档目录的,有建立重叠新文档的,有建立相似文档仓库的。所以在领域拆分和合并过程中,文档的稳定性其实也决定维护和迭代的稳定性。
上面画的几张图实际上是这两天现学的,我把它戏称为裂纹鸡蛋图,实际上叫做领域上下文图,很多大佬的一些文章多少都会采用这种形式的图去讲解领域上下文。我想学DDD的人不会画这种也说不过去,起码要向大佬看齐。所以也尝试画过几次,一开始用的processon画鸡蛋图很艰难的画完成了。但是processon的线条太智能化了不好控制锚点,只能连到指定位置,有点生硬。后来专门搜了一下画图工具软件,有个菜鸟教程的在线画图工具尝试了一下发现不能撤回,一旦画错整个画布都要废弃掉。所以又尝试了下亿图软件,mac版下载之后尝试画了下这种图发现还挺顺手。但是导出的时候就不友好了,默认加了水印。不过试用版对于画这种图已经可以了。