当前位置: 首页 > Web前端 > HTML

React背景下前端DDD的长期探索心得

时间:2023-03-27 22:41:15 HTML

??????介绍|腾讯前端工程师唐爽在React项目中尝试使用DDD方法论对业务对象进行建模,其团队形成了良好的业务沟通规范和业务逻辑沉淀流程,构建了更加稳定的业务体系。作者总结分享多年积累的探索经验,从业务的思考和react项目的特点出发,阐述前端DDD在项目中的探索。欢迎阅读交流。前言我们所在的业务团队服务于腾讯某投资部。该系统涉及的投资相关业务在国内具有典型意义。是涵盖一级市场、二级市场、基金的多品类、全流程投资体系。包括投资项目本身的业务处理,投资过程的工作处理(类比OA系统),以及大部分其他系统需要考虑的技术建设(如基于安全考虑的数据、合同文件、电子标志等)。方方面面都让我们在搭建这个系统的时候不仅需要技术本身,还需要具备掌握业务的能力。虽然技术团队跟随产品团队完成技术开发,但是如果在一些具体问题上不熟悉业务,在开发过程中很难真正处理好一些具体的逻辑代码。对于招商系统来说,准确实现业务是最基本的要求,否则风险可想而知。这也给我们的技术团队带来了巨大的挑战:如何在如此复杂的系统中合理把握每一个技术细节背后的业务逻辑,以保证业务实现的准确性?我们将目光投向了DDD,这是EricEvans在_Domain-DrivenDesign:TacklingComplexityintheHeartofSoftware(2004)_中提出的一组概念和方法。我们尝试探索在前端实践DDD的可能性。DDD的理念可以帮助我们的团队合理形成良好的业务沟通规范和业务逻辑沉淀流程;DDD的技术指导帮助我们构建更稳定的业务系统。基于这样的想法,我们开始了相关的??探索。什么是DDD?领域驱动设计是一种基于实际业务,从解决领域问题的角度来思考和设计系统的方法论。它包括两个方面:传播方法论和研发方法论。Eric要求我们的技术人员从另一个角度重新思考他们的工作方法。对于面对复杂业务系统开发的技术人员来说,不能一上来就开始系统设计和代码实现。他们要做的第一步是与领域专家合作,基于所有人都能理解的专业语言构建一套领域模型。这就是我们所说的“沟通方法论”。而这一步是我们现在很多开发团队根本没有考虑到的。首先,这里的“领域”指的是一类事物的集合,比如我们常说的金融领域、通信领域、数学领域等等。你可以清楚地感知到“域”就是“边界”,意味着一些“共性”和一些“特性”。领域专家是这些特定业务领域的高级工作者。他们对这个领域的业务有很好的了解,是这个领域的专家。为了让技术人员和领域专家坐在一起构建对应业务的领域模型,需要在各自的工作范围内摒弃各自狭隘的概念,找到一种彼此都能理解的表达概念的方式,最终确认各自的提议,问题和答案都能被对方准确理解。在我们看来,这是世界上最有效的沟通方式,没有之一。我们可以使用DSL(DomainSpecificLanguage)来完成这种通信。领域专家和技术专家基于统一的语言构建领域模型。可以想象,这个模型不能直接作为代码运行。开发者需要做的是用代码准确地实现领域模型。Eric在技术实现时充分考虑了系统与现实世界的差异,提出了很多技术建模方案(Schemes)和配套的架构概念,让技术专家有意无意地与领域专家一起构建领域模型。指导领域专家构建更符合后期技术实现的模型架构体系。图1统一语言、领域模型和代码实现的关系因此,DDD不是某种架构,而是一种设计。不同的业务团队可以基于此设计实现符合DDD的架构,以帮助他们更好地构建自己的系统。在阅读本书的过程中,我们反复体会到这本书是为技术人员而写的。看似是一本方法论的书,看似满是技术字眼,实则是传播工作方法的工具书。考虑到这一点,阅读本书,尤其是后面的章节,会让你变得越来越困惑。因为你在这本书里已经不是在寻找一些具体的实现或者架构,而是在思考如何去发现规律。它不能帮助你完成某个架构,但可以帮助你思考如何设计这个架构。现在回到我们的业务上,为什么DDD正好可以帮助解决复杂业务的系统设计呢?业务的技术语义1)从技术上讲,什么是业务?商务(Business)特指商业活动,是企业生产实现到利润回收的一个环节。它的总和构成了企业营利活动的全过程。一般来说,我们所指的业务是企业商业活动的一部分,有的甚至小到一个环节,比如“结算”环节。业务系统是辅助这些业务活动的计算机联机系统,以信息化的形式对企业的业务活动进行管理和决策(理论上,企业没有业务系统也可以运作,但在信息社会没有业务系统,这将使企业难以前进)。业务模块:从业务系统的构建者(领域专家、系统工程师等)的角度看业务系统时,按照某种业务活动的边界划分一个庞大的业务系统的单元。但从技术上讲,模块一般是一个粒度比较大的单元。一般来说,业务模块包含了系统中关于业务的所有内容,并且与其他业务有明确的界限。理论上可以脱离其他业务模块独立运行。业务逻辑:是仅通过代码实现的真实业务的规则映射。简单的说,一个业务存在什么逻辑,可以在纸上画出不同业务对象之间的联系和约束,将这些联系和约束一一列出,形成一个列表,这个列表中的每一项,就是一个规则,而这些规则加起来就是这个业务的整个业务逻辑。既然是规则,那我们就可以在代码层面对规则进行管理。对于前端开发者来说,最熟悉的规则管理就是路由管理。业务系统:本质上是一种基于人机交互的管理工具。不同角色在系统中管理的内容不同,但总的来说,都是基于上下游业务数据,在权限范围内进行业务管理活动。从个人角度来说,业务系统的开发是最复杂的,为什么呢?我们的前端开发对于不同的前端应用的开发有着非常明显的认知差异。前端应用分为三类,即业务系统、通用应用和工具应用。虽然都是前端应用,但是在开发工作中表现出的重视程度却大不相同。表1业务系统、通用应用、工具应用的横向对比由于开发过程中关注的重点不同,开发过程中解决问题的思路也大不相同。图2业务系统、通用应用、工具应用开发中的问题解决思路对比。这种解决问题思路的差异,决定了我们开发工作方法论上的差异。业务系统涉及对象多、连接密、维度广,导致实际开发中的复杂度远高于其他类型的应用。(但难度往往不高,业务系统往往不需要实现很多需要基于特殊技术实现的功能。)综上所述,业务系统复杂的原因,我们认为主要有以下几个方面:UI交互不复杂,重复实现之间存在矛盾;具体业务逻辑对原有架构的挑战;可持续维护(长期稳定性)和破坏性共存;模块间的数据和事件通知耦合;高业务准确性要求和不断变化的要求之间的时间竞争;前端和后端耦合。基于这些因素,我们需要谨慎对待我们开发的每一个功能。因为对于业务来说,这个功能可能会持续使用5年甚至10年,如果这个功能不够健壮,或者在设计之初没有充分考虑到业务发展需要的扩展能力,以后很有可能会用到。在迭代中给自己挖一个大坑。这是很多开发通用性强的大众化产品的开发者无法实现的。DDD在前端语境下的价值主张1)前端需要DDD吗?这个问题可以细化为,前端需要和业务端的领域专家沟通吗?在设计一个系统或功能时,是否需要基于通信结构的领域模型来完成模块的构建?我们需要在前端建模吗?我们需要在前端分层吗?等等,我给不出一个标准答案,但是我们想一想:在前端开发的今天,尤其是像我们这样的业务系统项目,如果还是按照设计稿去完成实现,是否能够准确的满足需求呢??或者说,对于前端开发者来说,我们是否有必要掌握业务细节,并通过一套方法论在代码中管理这些细节?我们想从另一个角度来描述前端开发人员的工作场景:图3前端开发人员的交流领域在上图中,我们描述了前端开发人员在整个业务开发过程中的情况。黑色圆圈表示这个过程中出现的角色,黑色实线表示两个角色在与前端沟通时的联系,虚线圆圈表示问题出现时涉及到的相关角色和讨论内容发生。可见,前端开发人员在与不同角色进行交流时,往往需要转换思维和视角。他面临的问题,他所处的境地,如果没有一套方法论的支撑,很难不在纷繁复杂的工作中迷失方向。2)前端可以DDD吗?DD作品,包括Eric的作品,讨论DDD的大多是在后端环境。DDD作为一种思想武器,可以帮助我们思考如何设计业务系统架构。前端是否只需要根据后端接口输出渲染接口?或许,前端根本就不能按照DDD来设计?并不真地。但是随着业务的深入,我们发现如今的业务系统(基于B/S架构)正在逐渐向重客户端(类似于C/S架构中的C端)方向靠拢。前端代码中的业务逻辑逐渐变得越来越丰富,甚至很多逻辑只能由前端完成,后端无能为力,或者前端和前端一起推广。运行在C端的前端代码承载着业务逻辑和UI混合在一起,导致组件或控制器的代码越来越大,难以维护。我们已经指出:对于基于vue编写的应用程序,当业务足够复杂时,你无法区分哪些vue组件是业务的,哪些是交互的。如果这种情况持续下去,组件将无法维护,代码将被破坏。作为前端开发者,我们需要思考如何让我们的代码组织更加健壮,更好的体现业务的核心逻辑?基于通信领域的思考,我们认为前端的主要内容包括:业务模型、数据服务、UI交互组件系统。图4前端关注的主要内容前端开发注定不仅仅关注业务模型。在前端,尤其是web领域,除了业务之外,还需要关注后端吐出来的数据和界面交互。甚至有这样一种情况,产品需求文档中写到,“当用户点击按钮时,需要弹出二次确认窗口,点击确认后完成签名传输”。在这个描述中,用户点击按钮弹出一个确认对话框。属于业务逻辑吗?从传统后端的角度来看,当然不属于。但是,如果去掉这个交互逻辑,这个业务流程是不是就可以完整的表达出来了呢?这是一个值得思考的问题。前端要实现DDD,照搬后端的做法是不可能的。前后端要解决的问题有很大的不同:表2前后端要解决的问题对比这也意味着在前端的背景下,我们关注的内容范围更广比后端。图5前后端DDD类别对比这些思考反过来又促使我们自问:DDD对前端的价值是什么?前端工程师需要重新审视自己的定位,跳出视觉交互实现者的陷阱,以工程师的身份面对自己。解决业务项目中的关键问题,与后端持续提供稳定可靠的业务服务,是前端工程师的工作。这些是我们从DDD中发现的。对于前端来说,实施DDD有肉眼可见的好处:稳定的业务知识体系;特异性;团队分工明确;快速响应需求变化;持续的敏捷性。这些收益对于需要不断迭代的项目团队来说是非常有价值的,尤其是需要不断支持业务团队完成更多以前无法完成的任务的价值,这是业务系统最不可替代的。前端领域建模我们在腾讯招商体系中实践了两年多的前端建模。与之前数据绑定的方式相比,这段时间的收入有很大的不同。在之前的开发中,虽然我们觉得麻烦,但是我们可以把功能堆起来。但是随着时间的推移,一些稍微老一点的逻辑就再也不敢动了。所谓牵一发而动全身,一点点改变就可能带来整个业务流程的瘫痪,或者影响相关模块的正确呈现。归根结底,我们的系统是线性的(大家可以回头看看上面线性思维属于什么类型的应用),我们开发团队的知识是无法复制的。开发人员负责业务功能,并且只有他了解业务。逻辑是什么?即使其他开发人员重新阅读代码,也很难梳理出准确全面的业务逻辑。如果需要重构,只能按照代码逻辑慢慢拷贝。两年前,我们开始思考我们遇到的大部分业务场景的共性。结合开发中遇到的痛点,我们迫切需要一种在应用界面切换和数据流转上有把握的方法。业务核心逻辑能力。我们开始了我们的建模探索。经过两年的积累,我们现在可以总结这些经验。1)思考单个业务的核心和边界在我们开始用代码建模之前,我们需要和领域专家(也就是业务方,或者和我们对接的产品人员)开个会,讨论一下什么是某个业务的核心概念以及它们可能发生的事件是什么,它与其他概念有什么关系?我们在记事本中整理了与待开发业务相关的各种概念以及它们之间的联系,使我们基本掌握了该业务的大部分知识。当我们开始用代码来表达这些知识时,遇到的第一个问题是:哪些是必须的,哪些与业务相关但不是必须的,我们应该用什么形式来表达这些概念?图6前端领域建模的首要问题是在核心和边界之间划清界限。这是使用OOP范例进行建模的一种相对常见且直接的方法。通过创建各种类来创建一个又一个对象。关键问题是,这些对象的核心是什么,边界在哪里?这些都是我们一开始就要考虑清楚的。2)建模方法DDD为我们提供了一些具体的建模方案,如ENTITY、VALUEOBJECT、SERVICE、AGGREGATE、REPOSITORY、FACTORY等。图7DDD建模方案在构建对象模型时,我们根据对象在业务中表达的含义选择相应的方案进行建模。比如我们对一个投资对象建模,首先要区分,投资对象的边界在哪里?比如在一项投资中,我们可能要经过一些审批程序,会产生一些特殊的数据。他们是否属于投资对象的核心?其次,我们需要了解哪些资源构成投资。比如有个字段叫“投资标的”。投资主体为投资中支付合同费用的公司。它也有自己的属性,那么投资主体是不是投资对象的核心资源呢?我们应该与投资实体打交道吗?它是由一个普通的JS对象表示的,还是创建了一个子模型来管理它?在我们的建模过程中经常会出现这样的想法,有时,我们也不能一蹴而就。有时需要在迭代更新中进行调整。在长期的探索和实践中,我们形成了一套标准的建模步骤(如下图)。在这个标准步骤中,我们抽象出建模的基类,在这些基类的基础上进行扩展,并根据每个对象的特点有选择地使用一定的建模方法。具体如下:图8前端领域建模步骤上图中完整呈现了代码层面实现领域模型的所有素材。建模的最小单位是Attribute,它是一个原子属性。原子属性描述了字段(Field)或属性(Property)的具体属性,即元数据中的一个项目。Meta是最小的模型,由Attribute组成,即关于单个字段的模型。Attribute和Meta不是单独存在的,它们是粒度最小的素材,Attribute一般不可重用(或不需要),Meta可以有限制地重用。上图中,Model的概念与本文提到的领域模型略有不同。模型是在代码层面能够表达一个完整业务对象的最小单元,它由Meta和其他资源组成。其他资源包括代码层面的方法(概念Factory)、静态属性(概念ValueObject)等。另外,与后端模型最大的区别在于前端模型必须为UI视图留出足够的空间层。我们为Model提供响应能力,以便在与UI结合时,可以观察到Model的变化,从而触发界面更新。具体从代码层面,我们以Mobx为例。图9两个简单的模型在上图中,我们创建了两个模型Todo和TodoList,其中TodoList是Todo的聚合,下面会讲到。这是两个普通的建模类。现在的问题是:如果我们在前端使用这两个模型,我们无法将它们与我们现有的UI框架一起使用,例如在react或vue中。有没有一种方法可以让我们以最小的成本扩展模型的功能?我们使用mobx库来转换模型。图10基于Mobx的简单模型使用mobx提供的装饰器,我们以最少的更改增强模型。这种变化几乎不干扰原始模型的原始阅读,但使模型可观察。结合mobx的工具,可以配合UI框架,在应用中无缝对接。图11基于Mobx的模型和UI对接不同于后端框架。后端使用控制器作为入口点来使用模型和视图。前端的主要消费者是UI框架,视图是入口。模型需要在UI状态后实例化以供使用。因此,像Mobx这样提供与UI框架接口的工具是更合理的设计方式。然而,这已经超出了建模本身的话题,这只是一个延伸。让我们回到建模步骤。在前端建模过程中,我们认为最难的一点就是如何划定一个模型的边界。在很多场景下,一个业务对象的某个逻辑依赖于另一个业务对象的某个信息。如何描述跨模型关系?在实践中,我们通过嵌套模型来表达,即聚合Aggregate。通过Aggregate,我们梳理出高于业务对象本身的逻辑。一般来说,聚合根包括业务的所有实体,即在核心实体上定义了业务的边界,业务只在这些实体之间运行。但是业务的运行不仅包括实体,还包括业务的流程逻辑。概念领域事件,以及其背后的领域服务——业务对象在业务运行过程中发生变化。对于前端来说,与后端的数据对接也是一个重点。基于Respository方案,我们建立响应式接口数据管理方案,创建一个Service单例来响应整个应用中的事件,传递业务对象的状态。让界面随着业务的流动而变化。以上建模结果被组织在一个Module中。模块不是单个文件,而是由一系列具有特定目录结构的文件组成的功能单元。不过需要注意的是,这里的Module与我们平时在应用开发中所说的Module略有不同。这里的Module主要是领域模型的代码实现完成后形成的组织单元。本质上还是建模的一部分,不同于我们通常的业务模块(包括UI等)。在React项目中设计业务模块React作为一个优秀的视图驱动库,实现了数据到视图的完美映射。与vue相比,优点是不需要数据本身的形式。在vue中,你给定的数据包含getter(Object.defineProperty)或者是Proxy创建的,或者是特定类实例化的,这都会导致vue失去部分响应能力。反应中没有这样的限制。因此,在实现DDD的过程中,亲和力更强。React虽然解决了数据到视图的映射,但是并没有提供视图到数据的反向映射的解决方案。在react中,我们会在onClick等内置的合成事件系统中执行回调函数,完成view到data的处理,但是这样的处理显然不利于建模。因此,在React本身之外,我们创建了一组基于RxJS的单例服务来处理从交互到模型层的事件绑定。在具体的React组件中,我们只向组件暴露它需要渲染和交互的数据(状态)和事件接口。我们将这个独立于React组件本身的系统称为“无视图交互模型”。模型写的时候,从视图层的角度处理业务模型的实例化和修改,从视图层处理交互事件等,但是在这个模型中,并没有具体的UI实现。如果你还有印象,你可能还记得我们在上一篇文章中问过“用户点击按钮弹出确认对话框是否属于业务逻辑?”。在“无视图交互模型”的设计下,我们可以将“用户点击按钮弹出对话框”的交互转化为模型的一部分。在这个模型中,它提供了用户点击动作的接口,该接口处理模型中持有的其他具体业务模型会不时调用,以满足需求文档中描述的需求。但是在具体的UI中,按钮长什么样,在什么位置,弹框的样式,都需要在UI层(组件)实现。开发者在实现时无需考虑按钮点击事件的具体效果,只需要调用模型接口即可。到此为止,你会发现在我们的业务模块中,并没有UI的具体实现。但是,我们几乎已经把业务相关的实体对象放在了需求文档中,以及业务流相关的一些交互。用代码表示。图12项目中实现业务模块的具体流程,从需求分析,基于统一语言建立领域模型,到用具体代码实现领域模型,再到建立无视图交互模型。至此,我们的业务建模基本完成,但是还没有任何界面效果。对于我们以前拿起react开始rolling的开发方式,简直是不可思议。怎么界面前面已经有很多代码了?我们的系统包括PC、APP和微信嵌入式H5。APP和H5的交互基本相同,细节上有一些差异,但PC端和移动端的差异不止一点点。但是对于某个业务模块,虽然两端的交互方式不同,但是业务逻辑(包括基于业务逻辑的交互逻辑)必须保持一致,否则业务本身就会出现问题。上图中虚线右侧是我们与多个终端共享的部分。各端业务模型的复用,不完全是为了代码复用,更多的是为了保证业务的一致性。虚线左边是我们常用的react组件系统的写法。针对不同的端点,我们的组件很少是可以复用的,基本都是各一套,但是我们可以保证业务逻辑没有出入。一旦业务(数据和交互)被业务模型标准化,对于不同端的视图层来说,就是一次换皮操作(有点夸张)。这个系统给了我们很大的想象空间。虽然我们团队没有更进一步,但是从个人角度来说,基于这个模型,我们可以实现更简单的基于后端的业务单元测试和输出。统一UIDSL、基于服务的业务模块、小程序等。基于DDD的前端应用架构分层也是DDD的一个重要思想。在Eric的原著中,他梳理了系统分层。同时,在业界,后来的后继者扩展了层次,主要有:依赖倒置四层架构、六边形架构、CleanArchitecture等。四层架构的典型示意图描述如图所示:图13DDD问题后系统分层架构示意图。但是在整体的层级划分上,前后端是一致的。图14遵循DDD的分层前端应用示意图我们之前的前端应用开发也是有分层思想的。但是,分层的做法非常薄弱,大多数情况下,所有的逻辑仍然混入到React组件和状态管理中。这也是为什么我们之前的代码经过一段时间的开发后可维护性会迅速减弱的原因。按照DDD的设计,我们对代码进行分层组织和管理。核心层是领域层,决定了整个应用的大部分代码逻辑。控制层和UI层虽然有一些逻辑,但大多是处理交互,与具体业务无关。完整的描述了这个系统所反映的业务场景。正因为如此,在多端复用时(比如PC端和APP端都写同一个业务),只需要重写UI层,其他层不用写。结论复杂的业务系统面临着巨大的挑战,无论是前端还是后端。除了系统本身的功能外,最大的风险在于业务逻辑需要准确实现,而研发团队的实现时间非常紧迫。后续系统的代码在长期的迭代中越来越难维护。DDD从沟通和研发两个角度为我们提供了方法论,是复杂业务的思维工具。它启发了我们,让我们在工作方式上不要开始胡乱写代码,而是先和业务方深入沟通,掌握业务的知识网络,基于统一的语言进行领域建模,再考虑如何实现它在react中结合起来开发了整个应用程序。在前端的背景下,由于前端所关注内容的异构性,我们不可能直接照搬后端的DDD实践,必须探索一种特殊的方式前端DDD。基于DDD的设计,我们的架构将不同的层分开,并在领域层和控制层充分描述业务需求。因此,可以脱离UI层进行业务编程,实现有效的业务单元测试。对于跨终端复用,就是换UI壳的处理,内部业务逻辑代码可以直接复用。以上就是我们在前端DDD这个话题下的探索。当然,DDD并不是唯一的选择。它激励着我们,让我们能够在它的基础上继续颠覆。欢迎在评论区分享交流。|由浅入深通读Vue源码:diff算法|优雅应对失败:如何为QQ音乐做一个高可用的架构系统?|QQ浏览器如何提高搜索相关性?|从Linux零拷贝:前端深入理解Linux-I/O技术盲盒|后端|人工智能与算法|运维|