|
作者:faryrong,腾讯CSIG后台开发工程师最近看了一本书《解构-领域驱动设计》,书中提出了领域驱动设计统一过程(DDDRUP),它指明了实践DDD的具体步骤,并很好地串联了各种概念、模式和思想。因此,我对书本内容做了梳理、简化,融入自己的理解,并结合之前阅读的书籍以及实践经验,最终形成这篇文章。希望可以帮助大伙理顺DDD的各种概念、模式和思想,降低上手DDD的门槛。1.背景领域驱动设计(DDD)由EricEvans提出,并一经《领域驱动设计:软件核心复杂性应对之道》的发布,在软件行业中引起了不少的轰动。DDD提供的一种新颖的,甚至有点“另类”的思维方式,它在告诉软件开发者“我们要用业务方案来解决业务问题,而不是技术方案解决业务问题”,有点魔法打败魔法的意思。DDD虽然让人眼前一亮,但是所提倡的理念有点“违背直觉”(对开发人员而言),因此,在当时并没有流行开来。后来,微服务架构的兴起,大伙惊奇地发现DDD是作为划分“微服务边界”的一把利器,并且DDD提及的很多设计理念与微服务架构十分契合,因此DDD逐渐被开发者们接受并流行起来。毫不夸张地说,了解和学习DDD可以算得上是如今软件行业从业者的一门必修课了。但是!DDD的学习曲线较为陡峭。作为一个小白,翻阅过很多相关的书籍、KM文章和分享,但始终觉得未得要领、一知半解。原因有二:a)DDD涉及的概念繁多,且不同概念的抽象层次不一样,如果我们直白地去理解,往往会感到疑惑,比如:子域和限界上下文都是用于将问题进行归类和收敛,他们的区别是什么?b)缺少过程指导,难以将概念有序的串联起来。作为方法论,DDD给出了设计思想,核心原则以及常用工具,但是却缺少细致有序的方法步骤,导致难以上手实践。幸运的是,最近看了一本书《解构-领域驱动设计》。这本书提出了领域驱动设计统一过程(DDDRUP),它指明了实践DDD的具体步骤,并很好地串联了各种概念、模式和思想。因此,我对书本内容做了梳理、简化,融入自己的理解,并结合之前阅读的书籍以及实践经验,最终形成这篇文章。希望可以帮助大伙理顺DDD的各种概念、模式和思想,降低上手DDD的门槛。2.DDD概要与实践感悟经典必读书籍《领域驱动设计:软件核心复杂性应对之道》的书名包含了两个关键词:领域驱动和复杂性,分别代表了DDD的核心原则以及解决的问题。2.1复杂性系统的复杂性往往并不在技术上,而是来自领域本身、用户的活动或业务服务。当这种领域复杂性在设计中没有得到解决时,基础技术的构思再好也是无济于事。而系统的复杂度体现在三个方面:规模、结构和变化。规模:指的是系统所支持的功能点,以及功能点与功能点之间的的关系。DDD通过子领域,限界上下文,聚合等模式对问题进行拆分和归类,不断收窄问题域,保证聚合边界内所解决的问题集合足够收敛和可控。结构:指的是系统架构。系统架构是否分层;若分层,每层划分的职责边界是否清晰;架构的基本管理单元是什么,它决定了架构演进时的复杂度。DDD通过分层架构,独立出领域层,且架构中的每层都有清晰的职责。整体架构的基本管理单元是聚合,它是一个完整的、自治的管理单元,当需要进行服务拆分时,可以直接以聚合作为基本单元进行拆分。变化:指的是系统响应需求变化的能力。快速响应变化的有效手段是分离不易变逻辑和易变逻辑,"以不变应万变"。而通过分层架构独立的领域层正是不易变的逻辑。领域层是对领域知识的封装,其提供的领域服务具有经验性和前瞻性,是对领域内稳定的领域规则的表达。而领域层以外的应用层和基础设施层则是易变逻辑的封装。保证核心的独立和稳定,通过在调整应用层和基础设施层来实现快速响应需求变化。2.2领域驱动领域驱动指的是以领域作为解决问题切入点,面对业务需求,先提炼出领域概念,并构建领域模型来表达业务问题,而构建过程中我们应该尽可能避免牵扯技术方案或技术细节。而编码实现更像是对领域模型的代码翻译,代码(变量名、方法名、类名等)中要求能够表达领域概念,让人见码明义。结合实践经验,以下是本人对“领域驱动”的一些见解:思维模式转变实践DDD以前,我最常使用的是数据驱动设计。它的核心思路针对业务需求进行数据建模:根据业务需求提炼出类,然后通过ORM把类映射为表结构,并根据读写性能要求使用范式优化表与表之间的关联关系。数据驱动是从技术的维度解决业务问题,得出的数据模型是对业务需求的直接翻译,并没有蕴含稳定的领域知识/规则。一旦需求发生变化,数据模型就得发生变化,对应的库表的设计也需要进行调整。这种设计思维导致变化从需求穿透到了数据层,中间并没有稳定的,不易变的层级进行阻隔,最终导致系统响应变化的能力很差。协同方式转变过去由产品同学提出业务需求,研发同学根据业务需求的tapd进行技术方案设计,并编程实现。这种协同方式的弊端在于:无法形成能够消除认知差异的模型。产品同学从业务角度提出用户需求,这些需求可能是易变的、定制化的,而研发同学在缺少行业经验的情况下,往往会选择直译,即根据需求直接转换为数据模型。而研发同学从技术实现角度设计技术方案,其中涉及很多的技术细节,产品同学无法从中判断是否与自己提出的业务诉求和产品规划相一致,最终形成认知差异。且认知差异会随着迭代不断被放大,最后系统变成一个大泥球。DDD通过解锁新角色”领域专家"以及模型驱动设计,有效地降低产品和研发的认知差异。领域专家是具有丰富行业经验和领域知识储备的人,他们能够在易变的、定制化的需求中提炼出清晰的边界,稳定的、可复用的领域概念和业务规则,并携手产品和研发共同构建出领域模型。领域模型是对业务需求的知识表达形式,它不涉及具体的技术细节(但能够指导研发同学进行编程实现),因此消除了产品和研发在需求认知上的鸿沟。而模型驱动设计则要求领域模型能够关联业务需求和编码实现,模型的变更意味着需求变更和代码变更,协作围绕模型为中心。精炼循环精炼循环指的是在统一语言,提炼领域概念,明确边界,构建模型,绑定实现过程中,这些环节相互影响和反馈,在不断的迭代试错-调整以最终沉淀出稳定的、深层次的模型的过程。比如,我们在提炼领域概念的时候会觉得统一语言定义不合理/有歧义,此时我们就会调整统一语言的定义,并重新进行提炼领域概念。通过精炼循环,我们逐步形成稳定的领域模型。在DDD中,让领域专家来主导概念提炼、边界划分等宏观设计,原因就在于领域专家的经验和行业洞见来源于过去已经迭代的无数个精炼循环,因此由这些宏观设计推导出来的领域模型,往往都是非常稳定的。精炼循环的核心是循环,它避免知识只朝单一方向流动,最终因各环节上的认知差异,最终导致模型无法在产品、领域专家和研发中达成一致、模型与实现割裂。2.3怎么才算DDD?我早期实践DDD的时候,认为代码分层遵循四层架构就是DDD,抑或分离接口和实现,实现下沉至基础设施层就是DDD,实则不然。结合上述内容,目前个人认为只要满足以下条件即为实践DDD:构建出产品、领域专家和研发同学认知一致且便于交流的模型,并且模型与实现紧密绑定;模型逐步演进,反复消化和精炼;模型蕴含领域知识,足够稳定。3.问题空间&解空间3.1问题空间&解空间问题空间和解空间并非DDD特有的概念,而是人们为了区分真实世界和理念世界而提出的概念。问题空间表示的是真实世界,是具体的问题、用户的诉求,而解空间则是针对问题空间求解后构建的理念世界,其中包括了解决方案、模型等。DDD提出的战略设计覆盖了问题空间和解空间,而战术设计则聚焦在解空间上。明确DDD中的概念是作用于问题空间还是解空间,更有助于我们理解它们。3.2示例-学生管理系统的问题空间学生管理系统(StudentManagementSystem,下文简称SMS)作为DDDRUP的讲解示例,以下为其问题空间的描述。学校需要构建一个学生管理系统(StudentManagementSystem,SMS)。通过这个管理系统,学生可以进行选课,查询成绩,查询绩点。而老师则可以通过这个系统录入授课课程的成绩。录入的分数会由系统自动换算为绩点,规则如下:若分数>=90,绩点为4.0;90>=分数>80,绩点为3.0;80>=分数>70,绩点为2.0;70>=分数>=60,绩点为1.0;成绩domain,application),(interface、application、domain—>infranstructure);第二,拆分服务更便捷。当我们需要部分领域独立拆分出来的时候,在实现层面就只需要关注infransturcture-impl模块即可。Infranstructure-impl模块依赖应用层的原因是应用层可能会抽象出防腐层接口,需要infranstruct-impl为其提供实现。8.2代码骨架8.2.1用户接口层用户接口层的核心职能:协议转换和适配、鉴权、参数校验和异常处理。├──controller//面向视图模型&资源│├──ResultController.java│├──assembler//装配器,将VO转换为DTO││└──ResultAssembler.java│└──vo//VO(ViewObject)对象│├──EnterResultRequest.java│└──ResponseVO.java├──provider//面向服务行为├──subscriber//面向事件└──task//面向策略└──TotalResultTask.java8.2.2应用层应用层的核心职能:编排领域服务、事务管理、发布应用事件。├──assembler//装配器,将DTO转换为DO│├──ResultAssembler.java│└──TotalResultAssembler.java├──dto//DTO(DataTransferObject)对象│├──cmd//命令相关的DTO对象││├──ComputeTotalResultCmd.java││├──EnterResultCmd.java││└──ModifyResultCmd.java│├──event//应用事件相关的DTO对象,subscriber负责接收│└──qry//查询相关的DTO对象└──service//应用服务├──ResultApplicationService.java├──event//应用事件,用于发布└──adapter//防腐层适配器接口8.2.3领域层代码组织以聚合为基本单元。├──result//成绩聚合│├──entity//成绩聚合内的实体││└──Result.java│├──service//领域服务││├──ResultDomainService.java││├──event//领域事件││├──adapter//防腐层适配器接口││├──factory//工厂││└──repository//资源库││└──ResultRepository.java│└──valueobject//成绩聚合的值对象│├──GPA.java│├──ResultUK.java│├──SchoolYear.java│└──Semester.java└──totalresult//总成绩聚合├──...这段有点长,其代码结构与成绩聚合一致,因此省略...8.2.4基础设施实现层该层主要提供领域层接口(资源库、防腐层接口)和应用层接口(防腐层接口)的实现。代码组织基本以聚合为基本单元。对于应用层的防腐层接口,则直接以application作为包名组织。├──application//应用层相关实现│└──adapter//防腐层适配器接口实现│├──facade//外观接口│└──translator//转换器,DO->DTO├──result//成绩聚合相关实现│├──adapter││├──facade││└──translator│└──repository//成绩聚合资源库接口实现│└──ResultRepositoryImpl.java└──totalresult//总成绩聚合相关实现├──adapter│├──CourseAdapterImpl.java│├──facade│└──translator└──repository└──TotalResultRepositoryImpl.java9.杂谈9.1DDD与微服务微服务拆解指的是把一个单体服务拆分为粒度“足够小”的多个服务,而这里的“足够小”是一个主观的,没有任何标准的定义。尽管如此,我们对“微”这个词还是有一些基本要求的:足够内聚,足够独立,足够完备,这才使得拆分出来的微服务收益大于投入,试想如果一个微服务提供的业务功能会牵扯到与其他众多微服务的协作,那岂不是芭比Q了。而上述我们对微服务的基本要求,实际上与限界上下文的特征(最小完备,自我履行,稳定空间,独立进化)不谋而合,因此,我们可以把限界上下文映射为微服务。我在日常实践中,都是将限界上下文和微服务的关系进行一一对应的,但这不是绝对的!限界上下文是站在领域角度给出的逻辑边界,而微服务的设计往往还要考虑物理边界,以及实际的质量需求(性能,可用性,安全性等),比如当我们采用的是CQRS架构,领域模型会被分为命令模型和查询模型,虽然它们同属一个限界上下文,但是它们往往是物理隔离的。因此,限界上下文只能作为微服务拆分的指导,而拆分过程中需要考虑质量需求,架构设计等技术因素。9.2事务9.2.1本地事务上文在提及限界上下文识别和聚合设计的时候其实都提到需要考虑事务属性,即需要通过本地事务来保证业务规则的不变性/一致性。这里我们会疑惑的是:谁来承担管理事务的职责?事务管理的边界是什么?应用层承担管理事务的职责事务本质是一种技术手段,而领域模型本身与技术无关,因此事务应该由应用层负责管理。事务管理的边界是聚合,有时限界上下文也可以资源库操作的基本单元是聚合,因此事务管理的边界是聚合便是自然而然得出的结论。这里需要考虑的是当需要保证事务属性的不仅仅只有资源库操作,还包括发布领域事件时(即保证聚合落库和事件发布的原子性),我们可能需要采用可靠事件模式,即通过把领域事件落库事件表来表示事件的发布。此时应用层在管理事务时就没什么心智负担了。当然,采用可靠事件模式实际是限制了领域模型的实现,也算是技术对领域模型的一种入侵吧,但相比于解放应用层而言,应该是利大于弊。我们也知道,应用层的核心职责是负责编排和协调不同聚合的领域服务,而应用层又负责事务管理,自然我们能推到出事务管理的边界是多个聚合(即限界上下文)。但这里有两个关注点:a)一般是出于质量需求(性能会好一些,时效性更高一些);b)同一个限界上下文内的多个聚合共享一个DB。9.2.2Saga事务为了避免耦合,DDD主张通过柔性事务来保证跨聚合、跨限界上下文的最终一致性。而目前业界比较主流的应用是Saga模式:通过使用异步消息来协调一系列本地事务,从而维度多个服务之间的数据一致性。而另一个非常著名的柔性事务方案TCC为啥没有Saga契合呢?TCC共分为三个阶段:Try阶段:准备阶段,对资源进行锁定或预留;Confirm阶段:提交阶段,执行实际的操作;Cancel阶段:补偿阶段,任意执行的操作出错了,就需要执行补偿,即释放Try阶段预留的资源。可以看到TCC实际对领域模型的侵入是比较大的:a)TCC要求领域模型设计时,定义相关的属性以支持资源锁定/预留的问题;b)TCC对服务接口定义做出了要求,领域模型需要提供Try,Confirm和Cancel相应的领域服务。Saga模式并不要求其对资源进行锁定/预留,而其补偿操作也是通过执行操作的逆操作来完成(比如支付的逆操作是退款)。而大部分情况下,完整的领域模型都会对外提供操作及其逆操作。10.参考《解耦-领域驱动设计》《领域驱动设计:软件核心复杂性应对之道》《实现领域驱动设计》《微服务架构设计模式》极客时间《DDD实战课》极客时间《如何落地业务建模》《领域驱动设计精粹》最近其他好文:深入揭秘epoll是如何实现IO多路复用的低代码是什么?有什么优势Go高性能编程技法
|
|