您还未登录! 登录 | 注册 | 帮助  

您的位置: 首页 > 业务知识 > 正文

降低复杂度提升效率,DDD在携程用车/租车订单系统重构中的实践

发表于:2023-04-14 作者:小白龙 来源:携程技术

随着历史业务不断迭代和业务场景越来越复杂,携程用车、租车(简称两车)面临历史技术债和系统复杂度越来越高带来的理解、维护、迭代困难等问题,我们开始寻求如何更有效的降低复杂度和提升效率的方法。

本文描述了两车如何利用DDD(Domain-driven Design,领域驱动设计)方法论降低系统复杂度以及在重构历史系统中的取舍和思考。对于复杂业务场景下的领域驱动设计具有借鉴意义。

一、案例介绍

携程用车订单相关业务包括接送机、包车、打车这些产线,订单相关的功能包括订单状态管理、支付状态管理、供应商订单状态管理、履约状态管理,其中履约状态中包含司机相关状态,完成订单需要将额外费用结清。

携程租车订单相关功能包括订单状态管理、支付状态管理、押金扣款记录、供应商订单状态管理、履约状态管理,其中履约状态主要是取车和还车相关状态。

订单和相关实体如下图所示:

图片

二、问题分析

由于两车业务存在一些差异,为了读者更容易理解,因此将抽取共性问题来说明。

2.1 沟通困难

关于沟通困难,我们发现整个开发过程中,沟通实际上是一个非常消耗时间的事情,需求方需要和产品沟通,产品要和研发人员沟通,研发开发过程中发现一些忽略的细节需要产品确认,来回之间耗费了大量时间。如果是跨团队沟通,这样的问题会更加复杂,以下总结了一些常见的场景:

  • 产品不关心研发的实现,但是觉得需求很简单或者很复杂。
  • 研发开发过程中发现一些忽略的细节需要产品确认,产品要找需求方确认。
  • 历史逻辑没人知道,需求评审的时候无法发现问题,做到最后发现有问题。
  • 跨团队之间不了解对方的业务,需要反复沟通确认。
  • 遇到同一个名词不同的理解导致无效沟通。
  • 一个需求到底该哪个域来实现是我们在实践中经常反复探讨的问题。
  • ...

例如订单和供应商订单在不同的团队内都叫订单,在沟通中针对“订单”的讨论就会产生歧义。

2.2 业务边界不清晰

设计之初,订单被各调用方当作了对外输出的数据源头,数据需求方只要调订单详情即可获取全量数据,这为以后订单的迭代带来了相当大的隐患。订单在自己的业务模型中加入大量不涉及自身业务的冗余字段,在系统的演进过程中,由于无脑插入他方业务字段使得订单自己也要维护相关的逻辑(解释和修改),导致各方对订单的耦合日益加深,导致订单服务的发布变成高风险行为,甚至一个无关订单业务的相关字段修改也可能导致系统故障。

例如订单上关于供应商的相关数据,用户订单有一份,采购订单也有一份,当采购要修改供应商的相关逻辑时要用户订单也一起修改,而用户订单必须排查和推动相关使用到这个字段的业务方切换替代方案。

图片

2.3 面对业务变化修改困难

随着历史业务迭代,订单中耦合了许多非订单关注的业务逻辑。例如历史上给用户发消息通知是根据用户订单状态变化触发的,由于和通知平台交互,因此订单要提供通知相关的所有参数,等于订单依赖通知相关的模版,明显存在核心依赖非核心的问题。而此时如果我们提出需求,要对于某些通知平台发送失败的消息进行重发,逻辑似乎也只能做到订单上,不论怎么看都很不优雅。

图片

三、解决方案

3.1 回归业务本质——挖掘愿景

为了解决业务归属问题和明确系统发展方向,避免将资源投入那些非核心的功能,我们需要明确当前项目它是什么,目标是什么。因此我们需要为系统准备一份愿景,它将指导我们在未来的迭代中不迷失方向。这个愿景相当于我们的产品定位,是我们的系统和其它系统不同之处,也是当前系统的边界。

图片

愿景就像手电筒中发出的光,在光暗之间是我们系统的边界,系统的未来也在光的方向中。

输出一个愿景说明有很多方式,为了简化落地的门槛,我们采取麦肯锡“电梯演讲”的方式,围绕机会、挑战、优势、劣势给出一组结果,由领域专家和开发团队一起进行头脑风暴,实际上这也是DDD统一语言的开始,我们必须从愿景开始就达成一致。

图片

友情提醒

Eric Even在他的书中曾提到过一种模式:领域愿景描述(Domain Vision Statement)

“由于一开始项目的模型通常不存在,但是需求是早已定下的重点,为了我们在后续阶段清楚了解系统的价值,以价值作为我们的导向。”

我们在研究领域愿景描述时发现要写出一份合格的文档并不容易,因为它缺乏明确的规范和套路,Eric也只是给了我们几个案例体会,不得不说虽然写出来容易,但是要做到合格还是有门槛的。因此我们退一步,回到Eric说的愿景说明来:“很多项目团队都会编写‘愿景说明’以便管理。最好的愿景说明会展示出应用程序为组织带来的具体价值。”

3.2 高效沟通——利用事件风暴统一语言

图片

说到统一语言,最经典的例子应该是传话游戏,一句话从最初的人口中说出,经历中间多人转述,最后可能完全变成另一种意思。

为了快速实现统一语言,我们在订单重构中花了比较多的时间进行事件风暴。事件风暴有以下几点优势:

  • 事件风暴围绕业务流程进行讨论,使在场的每一个人都通过多条流程深入了解业务实体的变化。
  • 事件风暴聚集了“领域专家”,产品、开发、测试等,本质也是一场集合集体智慧的头脑风暴,所有人在事件风暴中达成业务共识。
  • 事件风暴集合了所有人的领域知识,同样是一场领域知识的分享会。

图片

 

 

 

原本事件风暴是以工作坊的方式在线下组织,这样大家的参与感更强烈。但是由于成本和线上办公的兴起,我们在在线工作坊的实践会更多一些。这里推荐两个工具,一个是行知蜂(BeeArt),另一个是可画(canva),都支持多人在线协作。

事件风暴其实非常简单,就是业务流程+业务用例,将业务流程横向展开,通过用例将业务中的名词状态变化一一列举。其中色块的大小和颜色可以参考www.eventstorming.com,但是我认为只要能够统一大家的认知,颜色是次要的。

经过我们的尝试,先列举业务中单据的状态变化,后补全触发状态变化的动作和角色效率会更高一些。关键是将大家认知中的不同事物相同名词、不同名词相同事物识别出来,利于后续建模。

通过事件风暴,我们主要关注以下几种情况:

  • 沟通中那些脱离当前领域就难以理解的词汇;
  • 相同名词,含义不同的;
  • 名词不同,含义相同的。

将以上三种情况涉及的名词动词总结成统一语言表,特别是第三种情况恰恰是我们划分限界上下文的关键依据。例如我们在聊支付单时发现存在两种支付单,一个是包含我们业务的支付单,它需要记录当前支付的场景并包含一定的业务规则;另一个是支付平台的支付单,每次支付都会生成一个支付单,它可以认为是和更抽象的订单相关(例如会员订单、优惠券订单)。

于是我们提取了费项记录这个概念,表达一笔订单可以有多个费项记录,用于区分我们的支付

单和支付中台的支付单之间的差别。

3.3 自上而下细化边界——子域划分

传统面向过程的开发方法面对复杂系统通常会采用DFD数据流图的方式进行拆分,在DDD中则是提出了子域的概念。我们总是会听到领域(Domain)和子域(Sub Domain),不论是Eric的DDD还是IDDD中都大量使用这些概念,但是我们会发现他们并未向我们解释清楚子域是如何划分而来的。

对于一个已有的系统而言,我们可以根据康威定律得出:团队边界=系统边界,因此可以认为每个团队负责的部分就是天然的子域。由于目前订单团队本就分为用户订单组和采购派发组,因此我们可以初步得出一个领域划分:

图片

此时我们根据愿景,可以明确两个子域各自的职责:用户订单子域负责提供用户订单流程的查看和管理,并且负责在需要的环节主动通知用户;采购订单子域则负责真正定后履约流程的流转,包括供应商和行前行中行后的状态更新。

最后支付使用的是携程金融的能力,由于支付平台的能力在携程内部是统一的,因此我们认为支付平台属于通用域。

3.4 自下而上抽象概念——限界上下文

领域的概念相对而言还是模糊的,因此Eric提出了DDD中最重要的概念:限界上下文。而限界上下文并非凭空而来,而是需要对我们在事件风暴中得到的名词进行归纳而来。

图片

首先我们列举了用户订单域的各种用例,包括下用户单、支付订单、修改订单、取消订单等。

通过建模法归纳模型,例如在订单流程中我们存在多场景的支付,同时又依赖支付平台的支付单,因此我们得到了维护支付单状态的支付费项记录,它既维护了支付单相关的信息,也维护了当前订单系统内关于支付的业务逻辑。

最后我们根据业务相关性对得出的实体进行归纳,结合我们的愿景得出三个上下文,分别是:

  • 用户订单状态上下文:负责管理用户订单状态管理;
  • 支付费项上下文:负责订单支付相关状态管理;
  • 用户通知上下文:负责对用户进行多种方式的通知。

图片

3.5 挖掘业务变化的瓶颈——上下文依赖关系

实际上限界上下文可以拆到很细的粒度,但是我们应该遵循“奥康姆剃刀”的规则,尽量设置合理的数量,拆分的要有理有据。我们可以先看看原来的系统上下文依赖关系:

图片

根据Eric对上下文关系的总结,我们可以得出消息中心作为携程的消息中台,不会为了某个业务线做特殊逻辑,因此是很明显的遵奉者(Conformist)。此时消息相关的处理耦合在订单内部,如果发送消息没有业务逻辑那么采取防腐层(ACL)的方式是比较常见的。

由于我们已经识别到用户通知存在业务逻辑,因此订单直接和消息中心交互显得奇怪,而且订单作为核心域,本来就应该尽量不依赖其它域,对此我们进行了如下设计:

图片

这样用户订单上下文更加内聚,而用户通知也更加易于迭代。

四、收益总结

4.1 业务逻辑耦合降低

通过上下文拆分和职责的明确,由各领域维护自己的数据和领域知识,使得订单不再维护这些字段,而由数据写入的业务方去维护,后续有和订单无关的业务逻辑变更时订单无需改动。

4.2 团队效率提高

随着上下文拆分和康威定律的应用,各团队职责和各自的领域形成映射,过去由于团队职责划分不清,经常为某功能谁做来争论不休的问题也得到了解决。

4.3 性能和稳定性提高

通过上下文拆分后,订单实体从之前的780多个字段简化到200多个字段,大大降低了订单的维护成本,存储数据量减少,原来的业务逻辑也由每个写入方自己进行维护,接口性能p95 写由68ms优化到12ms ,读从63ms降低到5ms。

4.4 数据一致性

由于过去业务方写入数据到订单可能由于网络抖动等原因写入失败或业务方写错数据导致修复数据需要两边一起改,现在业务方将数据存放在自己的领域内,不再存入订单,避免了数据不一致和字段写错等问题的产生。

4.5 人力成本大幅下降

产研沟通涉及的相关方大量减少,链路缩短。刨去原本因为业务逻辑耦合导致订单跟着修改的人力成本,整体人力成本小项目下降70%,大项目更是下降80%。

五、遇到的问题和方案探索

落地DDD实际上是一个非常困难的过程,我们必须面对缺乏领域专家,业务需求多且急,团队对DDD理解不深等诸多问题。对此我们总结出以下几点经验:

5.1 领域专家难寻

领域专家是DDD中最重要的角色之一,没有领域专家我们就无法获取知识,就没有后续的建模等等。但是实际工作中要寻找一个严格意义上的领域专家是困难且成本高昂的,因此我们需要寻求一些其它方式曲线救国,例如该领域的资深研发,资深QA等,同时我们两车还采取互相借鉴的方式,虽然业务不完全相同,但是领域上也有互通之处。

5.2 业务需求多且急

实际工作中我们经常忙于各种业务项目,关键是还很急。这就很容易导致我们没办法专注于DDD改造,怎么办呢?我们的方案是在有时间的时候把方向定下来,提前进行设计,然后在做业务项目时将这些设计逐步进行实现。

5.3 团队对DDD理解不深

为了提高团队对DDD的理解,我们专门成立了DDD培训小组,将我们的一些落地经验整理成规范和最佳实践。同时在落地时由培训小组的同学进行把关,避免大家走弯路。

以上就是此次分享的全部内容,如果后续大家有什么疑问可以在下方留言,如果后续有机会我们会在疑惑较多的点进行再次分享。希望大家通过这篇文章得到一些想要的收获!