简介
DDD是一套方法论,一套思想。种类繁多的元模型和名词概念。其本质都是指导思想对应的解决方案“之一”,初学者容易被表象所困。应始终清醒保持认知“DDD各种元模型都是为解决实际开发中某类问题而起”。在接触各类元模型时应结合自身业务面临问题来求证,这样有助于避免被概念表象所困,回归解决问题的本质。
背景
数据架构团队从18年开始,受业务需求驱动开发电话机器人,转眼已近5年。目前,在该平台下已搭建100+不同类型机器人,为公司经销商、二手车、主机厂、金融等多个BU业务提供外呼能力,日外呼量几十万通。电话机器人项目已初具规模,但过程中也遇到不少挑战。为了应对这些挑战,我们团队最终采用DDD思想进行重构和开发。
在应用DDD的过程中,数据架构团队落地了一些自己的开发规范。这里就把一些经验和想法分享给大家,希望能起到抛砖引玉作用。这里解释一下,篇幅中很多元模型没有展开讲,也没有给出具体案例。一是考虑到篇幅长度问题。二是理解了DDD思想,结合各自业务来实现就好了,给出在我业务的例子意义并不大。此外这类案例很容易找的到。同时,觉得给出我们团队遇到的问题、解决方案,落地过程和我们形成的开发规范对大家来说更有价值。对DDD感兴趣,想了解更多或对本文有疑问的同学,欢迎找我交流讨论。
下面,我从这几个部分来进行分享:机器人项目中遇到的挑战、为什么是DDD、DDD落地步骤、对团队带来的提升、理论到实战遇到的冲突以及未来在DDD应用方面的改进和总结。
1.遇到的挑战
挑战一:业务逻辑复杂度高。随着各类业务的接入,为应对不同场景下的特定业务,不断追加新逻辑。
如:流程中的意图识别逻辑。
意图识别需要依赖AI的多个模型识别,多个模型识别出来的意图有可能是冲突的,需要对冲突的意图配置规则做取舍。同时,对一些冷启动或者紧急优化的场景,需要支持通过配置规则实时生效的方式来意图识别。并且在规则的意图识别中需要支持匹配词槽。词槽的类型又有多种,从优先级上区分有场景的全局词槽、流程上的词槽。从数据识别来源上区分,可以分为AI识别出来的,词典规则匹配出来的,还可能是业务方传递进来的。业务开展一段时间后,不同类型的词槽又增加不同属性,如车系的词槽有本品、经营范围、非经营等等;
挑战二:代码架构结构不清晰。随着业务需求功能的追加,伴随着代码规模增大。加之逻辑复杂和团队开发人员代码迥异,逐渐导致各种逻辑边界变得混乱。
如:我们通常的开发方式,按功能模块拆解,业务流程串联协调各个模块,共同完成业务需求。但是处理这类业务复杂的逻辑,这种方案设计有很大的弊端,模块边界很容易被穿透。
各模块关系相互调用,原本作为模块的隔离设计,实际在实现过程被完完全全的打破了。使原本理想中垂直拆分的模块,变成网状的结构。
模块负责人中间环节开发出来的的属性或方法,被外部其它模块外依赖导致功能发散出去。导致后期需求变动时风险增加,又或是发现被依赖了原本可以随意更改的方法不能变动,不得不增加额外逻辑代码来实现。造成了本就复杂的代码更加复杂。
对业务需求拆解不合理,需求功能在实现时就近开发,未严格按照模块拆解,缺少统一思想作为指导。
挑战三:产品需求多,难以辨别是否有真实价值。
挑战四:逻辑变化快,不少需求导致需要代码逻辑重新设计。
挑战五:业务多,各业务表述不一致,沟通成本高。
垂直边界被打破,代码复杂度增加,加上业务流程频繁调整。这些多重维度相互叠加,使得开发和维护难度指数增加。电话机器人这个一级应用系统的稳定性难以保障。即便技术同学都是资深的工程师,已经按照所能理解的微服务思想设计、按照模块拆解项目,即便代码逻辑中已经也引用不少设计模式来构建与扩展,即便已经是接入了公司各个平台质量工具、写了不少单元测试。但是在项目新需求迭代时,依旧出现不少“惊喜”,使整个团队很头疼。
2.为什么是DDD
为什么是DDD?每天那么多技术栈,那么多思想,为什么就是DDD来应对呢?首先DDD修饰的很好“软件核心复杂性应对之道”,使得不少人想一探究竟。所以来看看DDD是怎么来解决项目中遇到的挑战。
首先,我们来看看DDD对复杂度的归类,弄明白DDD要应对的复杂度是否是我面临的挑战。DDD相关资料中,从理解能力和预测能力两个维度来探索剖析复杂度的成因。
理解能力(就是软件系统对开发人员来说复杂难以理解):
第一规模:影响理解能力的第一要素。几百上千万行的代码,各需求点的关系相互影响。修改一处就会牵一发动全身。
第二结构:不合理甚至混乱的结构,导致开发人员对功能难以维护。
预测能力(就是业务的发展难以预测):
当需求变化时,难以预测软件实现的走向,会出现过度设计和设计不足问题。过度设计,预留了很多接口,构造了很多模式增加了代码的实现复杂度,但后来发现用不到。设计不足,需求的实现没考虑到后期的发展,当变化来临时需要推翻现有设计重新开发,被产品抱怨设计能力差。
DDD对复杂度的成因归结为:规模、结构、变化;规模和结构制造了理解能力障碍,变化制造了预测能力障碍,两者相加形成了复杂度问题。
其次,DDD并不仅仅是对代码设计阶段的理论,而是还包含从需求分析、架构映射和建模及实现的全流程设计指导。
需求分析阶段,通过相关指导思想提前准确获知业务价值,捕获未来变化方向。架构映射阶段,给出从需求到架构过程的指导思想,增加了设计权重和规范。通过子领域拆分、系统分层和限界上下文业务归类,给出指导规范,保障了系统架构的清晰,并且降低系统复杂度。建模及实现阶段,给出来领域驱动设计相关元模型,使各部分职能分工明确,快速响应业务需求和未来功能变化。
再次,来看DDD给出的指导思想:
规模问题:拆边界。以子领域、限界上下文对拆解分治。
针对分治思想,DDD给出两个重要的设计元模型:限界上下文和上下文映射。
结构问题:分层架构+限界隔离。
分层起到了隔离业务逻辑和技术实现复杂度问题。DDD引入的分层架构,将业务逻辑封装到领域层,支撑业务逻辑的技术实现放到基础设施层。在领域层之上的应用层封装应用服务,粘合二者进行协作。
变化问题:主动设计变化。
变化无法控制,只能拥抱变化。需求分析阶段运用5W思维识别变化规律,把控业务变化。DDD通过模型驱动设计元模型对限界上下文进行领域建模,形成结合分析、设计和实现一体的领域模型。
最后,来看DDD给出的解决方案。其引入了一套提炼为模式的设计元模型,对业务软件做到了对规模的控制、结构的拆分以及变化的主动响应。
简单介绍下这张图,整体分为两个大部分。第一部分是下面虚线圈出来的部分,不涉及具体技术实现。在需求分析阶段进行的,应对问题空间的一些元模型方案。另外部分,在第一部分的基础上,做具体系统架构分层、对象抽离聚合、服务拆解环节,这个阶段做对应的设计落地。
我的理解是这样,这套设计元模型给出了从需求分析、设计和实现一体整套解决方案。需求分析阶段的系统拆解(对应图中子领域元模型)。再拆到更新粒度的限界上下文。并给出各限界的协同关系方案(对应图中上下文映射元模型)。设计实现阶段给出模型驱动设计的设计元方案,通过系统的分层架构、领域服务、聚合等粒度的设计。给出一套完善的、有理论支撑的、可落地有标准的解决方案。
上述DDD对问题复杂度的剖析定位,完全就是电话机器人系统中的痛点。给出的解决方案,也完美解决业务面临的各类挑战。认识到其价值后,团队很快达成共识在后续的项目中进行落地。
3. DDD落地步骤
元模型细节、业务限界的拆解不展开讲了,直接给出我们团队实战中的步骤和产物。
3.1第一步预研阶段
这部分我们的经验是团队中有人充当先行者,先花费精力做深入学习DDD相关理念,然后同步到整个团队。就我们团队来讲,调研阶段时间比较零碎不好评估用时多久,团队科普阶段前后4次用时8小时。之后,团队内同学在有概念指导的基础上,已具备快速深入学习能力。并组织团队成内相互探讨印证理解。
3.2第二步引入指导思想和落地规范
3.2.1 需求分析阶段引入5W模型理论支撑,有助辨识出真实需求,主动把控变化方向和排除无意义需求。
这部分就是5W理论作为跟产品分析需求的理论的支撑,非常有助于识别出真实的需求,更好的分析出业务的发展方向。也能从源头上缩减无效需求,直接上图;
3.2.2 引入服务规约,以文档型对照代码业务功能实现。有助于开发及后续需求梳理,同时也能作为单元测试覆盖度的考量。
- 3.2.2.1 团队成员共识,需求先写服务规约,再做开发。写服务规约的时间,其实就是技术对需求理解的梳理,理清了思路,后续写代码时把这部分时间赚回来。
- 3.2.2.2 服务规约及需求,服务规约即对应单测。顺带解决了先前单测没有标准(我理解的代码、方法覆盖率这类,不能称作为标准)的问题。
这里给出我们团队采用的服务规约模板:
编号:标记业务服务的唯一编号。
名称:动词短语形式的业务服务名。
描述:
作为<角色>
我想要<服务功能>
以便于<服务价值>
触发事件:
角色主动触发的该业务服务事件,可以是点击UI的控件、具体的策略或伴生系统发送的消息等。
基本流程:
用于表现业务服务的主流程,即执行成功的业务场景。也可以称之为“主成功场景”。
替代流程:
用于表现业务服务的扩展流程,即执行失败的业务场景。
验收标准:
一系列可以接受的条件或业务规则,以要点形式列举。
3.3第三步确定架构方案
学习DDD中模型驱动设计元模型的方案。主要是划分职责边界,也就是限界上下文,达到从传统网状结构关系变为垂直切分关系,减少彼此依赖。整体采用限界上线文拆解加菱形驱动设计,形成整体思想指导。系统采用分层架构 COLA 4.0
3.4第四步共识命名标准形成团队编码规范
团队内共识包命名、类命名、出参入参的消息契约等规范。这里想说的是参考标准就是没有标准。希望大家先能理解DDD思想,然后参照学习业内共识性高的命名方案。同时需要兼顾团队内成员编程风格喜好,最终来制定自己团队的编码规范。
依我们入参、出参消息命名来举例。综合各方考量,并没有采用上图粒度特别细的命名方式。而是团队内简单共识为入参*request,出参*reponse命名标准。
3.5第五步结合业务特征识别出限界上下文
基于DDD思想,对业务进行事件风暴,在统一语言的指导下做全局需求分析、架构映射设计,识别出业务的限界上下文。
技术同学结合自身业务来设计,参照Demo资料还是比较容易找的到,这里不再赘述。这里给出识别限界上下文的一个指导过程,V型映射过程。
3.6最后进入建模的实现阶段
建议采测试驱动开发的方式进行编码,即用红绿黄驱动;
该方式遵循其三定律,这样能改善对需求的设计不足和过度设计问题。
定律一 | 一次只写一个刚好失败的测试,作为新加功能的描述。 |
定律二 | 不写任何产品代码,除非它刚好能让失败的测试通过。 |
定律三 | 只在测试全部通过的前提下做代码重构,或开始新加功能。 |
4.对团队带来的提升
4.1被动接收需求到主动应对
需求分析阶段,运用5W原则。剖析需求的合理性,能主动把控项目的变化方向。解决“挑战三”对需求价值辨别和改善了“挑战四”的把控业务发展变化方向。
4.2降低沟通成本
运用统一语言思想沟通,降低“挑战五”的各个环节的协作成本。
4.3架构设计提升
通过设计元模型的子领域模型、限界上下文合理拆解代码规模。通过DDD的分层思想,隔离业务逻辑与技术维度复杂度,清晰代码结构。同时项目采用菱形对称结构,通过南北向网关与外部交互,避免了模块的网状情况滋生。解决了“挑战二”问题和降低了“挑战一”复杂度问题。
4.4技术实现提升
团队在开发业务功能时,会考虑需求放到那个限界合理。实现过程会考虑放到领域层还是业务服务层,功能的实现上采用贫血模型还是充血。
4.5 文档规范提升
文档规范上,引入服务规约机制。既能作为梳理需求的工具,又能作为单测的依据。同时还为后期提供了服务说明的文档。
4.6代码实现提升
代码实现上,从架构到编码实现、命名,形成了一套有标注的规范。
总的来说,该模式下,团队的思维方式发生了转变。通过应用各类元模型,来应对从需求分析到系统架构、代码实现不同环节带来的挑战。
5.理论到实战遇到的冲突
5.1贫血模型 PK 充血模型
贫血模型:通俗来说,就是domain object只有属性的getter/setter方法的纯数据类,业务逻辑和应用逻辑都放到服务层中,这种模型下的domain object被Martin Fowler称之为“贫血的domain object”。
充血模型:反之,充血模型中不仅包含了对象的属性,还包含了对象的行为,包括业务逻辑。
从面向对象角度分析,对象是包含属性和行为的,理应是使用充血模型,并且DDD原则上也是建议采用充血模型。但落地到具体开发现状,即便是贫血模型有很多问题,但业内存在这么多年、运用这么普遍,总归是有其存在的价值。加上JAVA应用大部分采用Mybatis技术栈,很多对象是插件自动生成的贫血实体。所以问题来了,采用充血模型意味着一部分便利工具的废弃。这个问题团队内分歧比较大。最终我们的方式是这部分不做硬性标准,但建议使用充血模式。
5.2严格遵守数据转换约束
PK 精简提效的外部数据直接使用
在DDD的思想中,为了确保领域服务的可靠性。要求领域服务依赖的数据为领域内的实体、聚合数据,不允许直接使用外部的消息锲约数据。对应到菱形对称架构的南北向网关获取数据的转换,会带来额外的工作量。有团队同学建议某些相对稳定的结构可以不遵守该原则,理由是能提高开发速度,且认为可能90%的数据都是如数据库这类结构较为稳定的资源。但最终团队内还是严格要求遵守该指导思想。
5.3缓存处理允许共享 PK 限界隔离
同一系统不同限界中缓存处理:允许共享 PK 各限界隔离。
就当时场景来看,允许共享短期内能减少部分工作量、节约资源等优势。但之所以要划分限界,就是为了拆解关系防止过大。这里给到的建议是,首先考虑共用数据的服务是不是合并为一个限界比较合理。如果不能合并,必须隔离数据。
5.4服务规约对照需求的前端 PK 后端
指导理论思想很美好,需求分析时要求屏蔽技术实现思维。但终归是要落地到技术栈的,落地到技术实现时就会受技术实现的干扰。当时比较突出的一个问题,功能的实现可以放到前端,也可以后端服务实现。
举例一:需求要求“id+名字”组合展示,但是后端接口返回的id、名字两个字段,实际前端技术栈来组合,那面向前端与后端的服务规约不一致。
举例二:需求要求验证参数非空。在一些内部系统中,我们团队技术都是前后端全栈工程师,分工按需求模块开发。往往不会特别严谨到两端都做验证。也导致服务规约面向哪端有冲突。
我们最终的取舍:团队采用面向后端服务层面。但同时做一些改进,如验证这类功能转移到接口层面来实现。
5.5谁来确保服务规约编写是否正确产品PK 技术
最开始阶段理想状态是由需求侧产品来核验,本着谁的需求谁确认原则。但由于存在4.4的差异问题,我们实际落地是由技术负责人来审核。
6.未来在DDD应用方面的改进和总结
DDD的应用,团队目前做到了从架构和规范上面进行落地。但一些细节如:聚合类、实体、值对象这些设计,并没有特别精细。后期会进一步推进在这些细粒度上面的改进。同时,对一些在用的老项目,按照DDD思想进行改造重构。
有人认为应用DDD会降低开发效率,这个也是很多团队的一个顾虑。我们是这么看待这个问题的,应用DDD的场景是解决复杂性业务问题的,确实是会增加代码量。但不等于降低开发效率。清晰的架构结构、聚合的领域服务和规范的标准,对后期需求升级、代码维护、复杂度控制带来的收益,远大于投入。并且,软件行业给出的数据,80%的时间是在需求分析和设计,开发时间只占到20%。因此这部分损耗不是重点。
最后,陈述一下使用DDD的感受。DDD各种元模型种类繁多,大家可以根据业务面临的痛点有目的来学习和采用。在实际的业务环境中,我们的领域模型或多或少的都有一定的“特殊性”,如果100%的要符合DDD规范可能成本会比较高,所以最主要的是理解DDD思想,最终选择合适自身业务的方案。
作者简介
李晓华
- 经销商事业部-经销商技术部。
- 2016年加入汽车之家,目前任职于经销商数据架构组团队,负责电话机器人项目。