1. 前言
Domain-Driven Design,简称 DDD,翻译过来就是领域驱动设计。这个概念是从技术的视角提出的一种系统架构设计方法。网上能查到的大部分资料基本都是技术视角来阐述,列举的案例也都是代码形式的案例,非技术人员很难看懂,而技术人员去看,不同的技术人员理解上差异很大,之后在落地系统设计的时候往往无从下手,有时又感受不到 DDD 具体带来的优势,甚至一些尝试失败后,可能对 DDD 持负向态度。
DDD 是一种处理高度复杂领域的设计思想,它试图分离技术实现的复杂性,并围绕业务概念构建领域模型来控制业务的复杂性,以解决软件难以理解,难以演进的问题。因此 DDD 应该是以业务重塑为主,以系统重构为辅,在团队内对业务的运营方式、系统分工及边界建立共识和原则,同时兼顾业务未来发展的扩展性的系统工程。
本文将结合实际的基于 DDD 思想的酒店报价重构项目,从 DDD 推荐的战略设计(借助头脑风暴等确定模型,相当于确认需求)、战术设计(确定架构模式及代码规范,相当于确认技术方案)、系统实现三个核心阶段讲解 DDD 在整个过程中的巨大作用,同时涉及各个阶段目标及产出,希望这些实践能对读者有一定的指导作用。
2. 重构背景
先来看一下重构前梳理的报价计算的主要过程:
从上图中可以看出报价系统的业务比较复杂,可以看到一个现象:只要跟“价格”相关的业务都耦合在这里,但不同业务关于“价格”又有各自不同的含义。按照业务视角拆解一下:
从这两个图可以看到,报价系统做了很多不是“价格”的业务,对接了很多不同的业务团队。站在各自团队的角度来看,自己的业务需求涉及到了“价格”自然而然就去找了报价团队,每个团队也只关心自己的 KPI 是否达成,需求是否实现。技术团队似乎又不能拒绝业务提过来的需求,每个需求看起来都很合理(至少表面上是)。结果就是系统变成了一个大杂烩,出现如下的问题:
系统相互耦合,不同业务相互侵入,维护难度大。例如:免房本是流量置换来的底价为0的资源,免房团队如果按照代理商的逻辑给到报价,报价就不需要处理,但免房团队需要考虑售出量和免房的收益,就提出要比最低底价的代理商低1块钱,而其他代理商的底价是用户请求时实时计算出来的,为了实现这个逻辑,就要侵入报价的计算逻辑
开发效率低,容易踩坑,出现问题不好排查。业务没人收口,各个业务团队各自出需求的结果就是没人说的清楚,这些耦合的逻辑是如何关联作用的。导致方案阶段几乎看不到隐藏的坑,而到开发的过程才能发现,理想情况是需求变更加工时,悲惨情况是整个方案变更,之前开发的全作废,成本极高
核心业务被边缘业务侵入,拣了芝麻丢了西瓜。报价核心计算经常要被一些边缘的业务侵入,对整体计算进行调整和适配,导致核心业务与边缘业务杂糅在一起,互相影响,每次调整时都要把相关业务拿出来讨论影响及适配。
流程过于复杂,实际的定价策略受影响。
出现以上问题的原因:
需求多变化快,系统又涉及多个领域,业务逻辑比较复杂,系统和业务领域没有划分清楚。
核心原因缺少业务架构和成熟的运营体系。系统最终还是用来支撑业务,解决效率问题的。如果被支撑的业务是混乱不堪的,那么系统注定是不堪的。因为业务上缺少稳定的运营流程,导致业务分工上的职责不明确,进而导致系统分工的不明确。
为了能有效的解决上面的问题,我们开始了基于 DDD 思想的重构,核心是:产品和技术坐在一起,讨论业务玩法并达成共识,借助“域”的概念,搭建新的模型完成对业务进行重塑,既能满足已有核心业务,又能为未来业务发展做好规划。过程中,划清业务界限和定位,之后共同作为领域专家守住领域,对不合理的需求说不!
3. DDD 之战略设计阶段
3.1 头脑风暴:领域专家为主,技术为辅的梳理与讨论
在这一阶段,最核心的角色就是领域专家,重构之前主要是产品经理(PM)这个角色。这个阶段要做好,像普通的需求那样,大家一起简单的讨论一下,方案可行就去实施,这样是不 OK 的,容易陷入为了 DDD 而 DDD 中,效果不一定好。基于 DDD 思想进行重构,需要注意以下【两个前提】:
对核心业务运营的逻辑有深刻的了解。产品要能输出核心业务玩法和未来的规划,这样讨论才会有明确的方向,制定的模型才能符合当前需求及未来调整。
对现有一些业务需求的来龙去脉了解。产品明确一些核心业务诉求及限于当前架构等各种因素采用的业务玩法,这样就可以有目的地的进行重构和改善。
这里需要注意【一个原则】:
业务原子原则,划清业务边界;避免业务职责重叠,过耦合。
同时需要注意【一个方法】:
提炼业务原子原则:通过业务本质进行划分,寻找业务本质的方法 - 基于业务开展的目的进行划分。
在这一阶段里,产品可以输出实际的业务玩法,包括核心的业务玩法、临时的业务玩法等,让产品和技术快速在业务的理解上达成共识,为后面讨论实际的模型做好铺垫,大家能知道新模型的重点该解决什么问题、要在那方面做好扩展。以报价为例举例说明产品输出的业务玩法:
三方限价:站在酒店维度,申请保持价格区间,保证商家权益。
商促:EBK(大型固定的活动,优享会、天天特价、门店新客),ES 活动(短时实用性活动,七夕特惠),主要是对用户画像打包,满足特定的用户身份或特定的场景的营销活动,拿到更低的底价,获得更高的利润。
权益云:价格不变,多给些权益(延时退房、免费早餐等)。
付卖:结算时不定价,只要不影响结算就可以,主要靠返现,尽量拿到定价权。
...
同时,这一阶段,还要对业务瘦身,确认无用的业务、无价值的业务、一些模糊的业务场景,一起拿出来看看是否还需要,不需要的优先去掉,避免对后续讨论模型造成影响。由于系统迭代的时间太久,这一部分需要技术认真梳理代码,辅助补充好相关的业务场景和玩法,产品做好决策,要不要以及未来规划。
3.2 构建模型:解决已有痛点,治理杂乱流程
重构的核心是要把之前杂乱的流程治理好。在这个过程中,还要收集产品侧和技术侧的痛点,统一模型时除了保证对业务玩法的覆盖和规划,还要能解决已有的技术及产品痛点。这个过程主要分两步:抽象化、标准化。
- 抽象化:对所有业务玩法进行业务抽象,先做初级合并,再做整体合并,尽可能的将类似的业务玩法往一起合并(此处可类比软件架构设计里的高内聚)。下图是讨论时产品和技术对核心玩法的抽象:
- 标准化:在有了抽象化的流程后,就可以考虑对整个流程标准化了。在报价这次重构中,经过几次讨论,我们最终引入 SPU-SKU 等电商的模型和概念,抽取报价可扩展模型。核心是将个性化的业务适配成 SKU(实际为库存单元,可类比 DDD 概念里的通用子域),基于具体的 SKU 进行标准化的操作,之后新业务玩法主要进行 SKU 适配。对于还不支持的业务玩法,在 SKU 里增加新的属性,去定义对新字段的处理即可,业务玩法扩展性极强。
通过上图也可以看出,新的报价“域”已经划出来了,每个“域”包含的属性及要做的事情也清晰了,从一个原始的代理商价格,到经过处理最终展示给用户的价格,整个过程也清晰了。
这些完成,产品侧和技术侧已经基本明确本次重构可以达成的目标:业务重塑(含瘦身)、技术架构重建、解决已有痛点、业务达成共识。其实在这个时候,产品和开发基本已经对业务大部分达成共识了,沟通起来明显容易很多,后续阶段则是对这种共识的不断加强和深化。
4. DDD 之战术设计阶段
共识的模型出来,开始进入战术设计阶段。这一阶段,核心是基于战略设计阶段讨论出来的模型,确定实际使用的架构模式及代码结构规范。
我们采用的是比较流行的:分层架构+整洁架构,按照 DDD 推荐的结构分为 User Interface、Application Core、Infrastructure 三层,如下图所示:
在这个分层架构里,每一层我们又做了职责的确认:
User Interface:报价适配层,用来适配不同来源报价请求的个性化,主要体现报价的扩展能力。
Application Core:报价稳定层,报价计算引擎,主要体现报价核心的计算能力。
Infrastructure:报价基础层,主要封装对外部依赖的调用,包括相关组件、系统等。
这个模型里报价核心的“域”都在 Application Core 里,我们按照之前的域模型设计,结合实际的计算流程和业务形态,做了进一步细化:
定义好实际的架构模型后,开始确定代码结构规范。我们内部的核心要求是:约定大于规范,并兼顾开发习惯。具体为:
不严格遵守 DDD 推荐的代码规范:原因是 CQRS 缺 C(command),报价这里主要是计算,没有更新操作。
定义核心域,提供相关 service 方法,不需要聚合根(划定的几个域在实际模型计算时串行使用)。
使用统一上下文,贯穿计算全流程。
保证层级规范:区分接口层、应用层、基础层。
抽象核心报价计算流程,复用计算能力。
至于 可编排 与 可视化,主要是在开发过程中处理,我们也放到研发阶段去具体谈。
5. DDD 之系统实现阶段
这个阶段,核心角色正常已经变为技术,但是产品依旧需要结合最终的模型的去和技术不断的对实际流程里的细节。这里单独提一个报价这边做的不错可以参考的地方,产品非常负责任的把报价计算过程中所有细节进行了文档化,该举例子的举例子,该画图的画图,既是开发过程中的核心参考,也是之后新人学习提供了极大的便利。产品这么认真,开发自然也不差,将代码设计、核心流程也都通过各种图表记录下来,非常的实用。产品和开发在这个阶段产生的文档及图表,潜在的价值特别大。
这种大规模的重构,自然容易在过程中遇到一些问题,这里对几个核心的问题及应对做下总结:
新需求适配:对于开发人员,过程中新需求适配可能会不太舒服,但是换一个观点,这些需求正好可以提前验证模型的适应性,让相关人员在这个阶段就能看到未来完成后的效果,反而不用担心上线后的效果问题了。
快速交付:拖的时间越久,越容易插入更多的需求,变向增大工时,因此必要的时候该加班赶工时还是得加班的,不能慢慢来。
额外产生的需求:重构之前,我们的自动化 case 都是接口级别的,验证起来麻烦很多,新模型流程标准化后,自动化 case 可以调整为模块级别的 case 了,这样可以让自动化 case 更有针对性更有效。因此我们又对自动化 case 进行了大重构,虽然投入了大量的时间,但是一次性投入做完对整体架构是有好处的,就是值得的。自动化 case 从接口级别改造成模块级别后,整体 case 量也降到之前的25%左右,执行自动化 case 时间也相应的缩短了很多。
计划投入的人力被其他高优需求抢走:这种意外不一定都能遇到,但真要遇到了还是需要积极面对的,我们在遇到这个情况时,加班是避免不了的了,同时呢,我们协调了一下开发和测试的节奏,让测试提前进入测试已开发完成的模块,同时让部分可置后的功能(比如工具适配)调整为提测之后与测试并行。通过各种协调,我们很好的解决了这个问题。
接下来说一下借助 可视化 解决日常排查问题的痛点。我们对排查工具也进行了完全重写,借助新模型将报价计算过程标准化,单独针对工具请求,将报价计算过程中涉及到的核心数据全部放到上下文 debug 信息总输出,之后按模块进行渲染。这样通过工具界面请求,就可以把报价计算每一个流程进行还原,快速获取本次报价计算核心数据,这样一个简单的工具实用性极强,后期上线后值班报价不展示类问题直接降了90%以上,因为大家都可以很容易通过工具去查明原因了。当然这个工具可维护性也极强,工具如下图所示(部分核心信息隐藏):
这部分还剩一个 可编排 问题。报价计算过程中,受限于不同的渠道等因素,涉及到的计算流程是不完全相同的,因此存在能力复用及编排的可能。我们整理了不同渠道的流程后,发现主流程及其他流程差别较大,且不同渠道的流程一旦确定是不会修改的,如果去做动态编排反而可能会引发故障,同时如果新增一个渠道基本0.5pd 就可以完成,因此我们虽可以通过代码支持编排,但实际中并没有投入时间去设计和完成编排这个功能,避免出现有名无实。
在开发及验证都完成后,我们按照指定酒店、指定小城市酒店、热门城市酒店、全量城市酒店这种灰度流量的方式逐渐放量进行验证,前后3天完成全量发布,整个过程平稳,没有出现明显问题。
6. 后评估与总结
这次重构跨度3个月,进入开发到全量上线花费了1.5个月,比预期提前一天全量,实际带来的内部效果这里就不表述了,单独从各方感受做下总结:
技术侧:完整学习业务的核心玩法,重写了整个报价实时计算,将整个计算过程标准化,核心报价计算过程及细节都在掌握之中。
产品侧:在新的架构基础上,可以更容易调整定价策略来满足业务目标,更多精力放在优化定价策略上。
运营侧:报价计算过程可视化,问题处理效率提升。
整体:增加了更多的领域专家,大家更了解报价这块业务的流程和细节,沟通起来容易很多,圆满完成预期目标。
这里,也总结一下 DDD 在这个过程中的重要作用:
产品和技术坐在一起去梳理已有业务及痛点、讨论业务玩法及未来规划。和以往的需求及流程不一致,常规的产品需求主要从业务收益、用户体验等出发,常规的技术需求主要以技术优化及解决某些问题为目的,这个特殊的需求可以兼顾两者的利益和未来规划。
帮助产品划清“地盘”,明确边界和职责。
帮助技术改进架构,变混沌为清晰。
完成业务重塑的同时,解决技术痛点、升级系统架构,实现双赢。
DDD 为核心系统重构带来了新的思路,希望有更多的团队能使用它并用好它^_^