当前位置: 首页 > 科技观察

DDD实践

时间:2023-03-12 02:28:10 科技观察

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