领域驱动设计(DDD)在百度爱番番的实践

图片

导读:领域驱动设计(Domain Driven Design - DDD)起源于2004年Eric Evans出版《领域驱动设计》,相比于在国外IT圈享有盛誉且行之有效不一样,国内IT圈了解DDD的人不多,落地实践的少之又少。最近几年随着微服务架构的普及和中台的兴起,DDD也成了各大技术论坛和微信公众号文章里常常谈起的话题。前端

DDD的热度是起来了,但业界介绍DDD的资料大多偏理论,缺少生产项目可借鉴的实践经验。所以大多人读了不少DDD材料后仍是一脸懵,怎么衡量DDD带来的价值?老板能赞成搞DDD吗?什么样的业务和团队适合DDD?DDD跟互联网强调的小步快跑快速迭代能搭吗?若是要实践DDD产研团队都要作些啥?研发写代码跟平时有什么不同?本文结合百度爱番番产研团队在过去一年多经历的从探索、推广到全面落地DDD的过程,尝试回答上述问题,力求给你们带来一些借鉴意义。java

全文约9500字,预计阅读时间25分钟。算法

1  初心:以客户为中心,产研团队如何高效交付需求

百度爱番番围绕营销拓客和销售提效帮助企业收集、扩充、清洗、培养、跟进和转化线索。一方面爱番番的业务特色是典型的企业级(ToB)业务,具备必定的复杂度。业务对象多,单个业务对象提供的功能多,单个功能面向的场景多,业务对象之间组合出来的业务流程多。而且会随着交付的功能越多而变的越复杂。另外一方面产品处于爬坡阶段,功能须要快速迭代交付到客户,从而快速得到客户的反馈。产研团队在资源必定的状况下如何高效交付更复杂的需求成为了主要矛盾。数据库

分析当前阶段需求迭代过程当中的问题,能够总结为如下几类问题:编程

  1. 业务逻辑不能从产品团队精准传递到研发团队,有时研发进行了一段时间开发才发现需求理解有误差,致使须要从新跟产品经理讨论需求。后端

  2. 产品团队和研发团队对于业务复杂度没有的认识不统一,产品经理认为一个需求的开发不难,理应在较少时间内开发完毕。微信

  3. 研发团队面对需求增加和变化时,缺少对业务逻辑的抽象,每每开发一个须要点须要改动多处,容易出错且开发效率低,代码维护性差。架构

  4. 需求文档和代码逻辑不匹配,线上功能的业务逻辑为何实现成那样没有依据可查,领域知识得不到沉淀,团队得不到可持续成长。app

2  探索:找到适合的开发模式


上述问题集中体如今两个方面,一是产品属于企业应用类,功能自己复杂,如何让产研团队快速理解业务、快速交付。二是如何让领域知识可以比较准确的获得开发实现,让代码有比较好的可维护性。借鉴业内处理复杂企业级软件的开发经验,加上部分团队成员曾经有过DDD使用的经验,团队决定尝试运用DDD设计思想来指导产研团队的平常需求迭代。框架

2.1  DDD是啥?

DDD是一种围绕领域建模来解决复杂业务交付的设计思想。读者不妨自问几个问题,什么是复杂?什么是领域建模?

1. 什么是复杂?如何理解复杂?

复杂多是现状业务就复杂,也多是业务日渐演变成复杂。复杂来自规模在变,好比几个业务对象的逻辑不复杂,几十上百个业务对象就会变得错综复杂。复杂来自结构化不足,好比下图所示,结构化的中国结比非结构化的意大利面更有序、易于大脑理解。此外,一旦协同方多了,如何协同不一样团队完成软件交付也是一种复杂。

图片

2. 什么是领域建模?

领域模型跟技术毫无关系,而是为了更有结构化的拆解和表达业务逻辑。业务逻辑来自现实世界里的具体场景,涉及可视画面、操做动做和流程。要准确表达业务逻辑须要先讲清楚每一个概念是什么,再创建概念之间的联系,基于这些关系再组合出更多的流程。概念、联系、流程就是领域模型。围绕领域模型去表达业务时也天然而然地把技术实现细节分离出去了。后续代码实现就是将业务架构映射到系统架构的过程,之后业务架构调整了能快速的调整技术架构。

3. DDD中的领域如何理解?

  • DDD中表示业务逻辑的领域概念是:实体、值对象、领域服务、领域事件。这意味着全部领域逻辑都应该在这四种对象里,统一称为领域模型对象,这将极大减小业务逻辑的蔓延。

  • 引入聚合进一步封装实体和值对象,让领域逻辑更内聚,起到边界保护的做用。聚合的引入使得业务对象间的关联变少。如何设计聚合见下面实践部分。

  • 围绕聚合的操做引入工厂和资源库。工厂负责复杂聚合的建立,资源库负责聚合的加载、添加、修改、删除。聚合内的实体状态变动经过领域事件来推进。

  • 引入应用服务,对领域逻辑编排、封装。供上层接口层调用。一个应用服务就是一次编排,一次编排就是一个用户用例。

4. DDD领域概念详细解释和举例

image.png

2.2  DDD如何开展?

DDD包含战略设计、战术设计、技术实现三个部分。战略设计侧重于高层次、宏观上去划分限界上下文,而战术设计则关注使用建模工具来细化上下文,经过领域模型来表达业务。技术实现主要经过分层架构来隔离领域模型表明的业务逻辑和技术细节。一个总体过程大体包括:宏观划分各领域 → 领域内划分限界上下文,定义上下文之间的关系 → 上下文内分析业务,识别领域概念,定义合适的领域概念 → 经过分层架构实现编码,并验证领域模型的合理性,必要时从新回到前面步骤重构领域模型。

1. 战略设计

战略设计是团队领导层或业务负责人关心的,该步骤须要针对产品愿景、业务要解决的问题域,规划核心域、通用域、支撑域,作合适的资源投入。

1. 什么是领域和限界上下文?

领域表明现实世界的特定问题和解决方案的集合,好比销售领域、营销领域。DDD里的限界上下文(Bouded Context)是对领域的软件实现,好比线索系统、商机系统就是销售领域内的限界上下文。限界上下文定义了解决方案的明显边界,边界里的每个领域概念,包括领域概念内的属性和行为都有特殊含义。出了限界上下文这个边界这层含义就不复存在。

2. 如何划分限界上下文?

1:根据相关性作归类。通常是优先考虑功能相关性而不是语义相关性,好比建立订单和支付订单都是订单语义,但功能相差比较大,应该划分为两个限界上下文。

2:根据团队粒度作裁剪、根据技术特色作裁剪。一些通用的技术功能应该尽量归拢到一个限界上下文,好比每一个业务限界上下文都有监控,但监控能力应该归拢到监控限界上下文。

3. BC与微服务什么关系?

微服务是包含高度相关功能的一个开发部署单元,有本身的技术自治性包括技术选型、弹性扩缩容、发布上线频率等,有本身的业务演变自治性。BC是根据领域逻辑的内聚状况造成的一个总体。一个微服务能够包含一个或多个BC,到底包含几个?须要根据团队大小、BC复杂度和技术特性来定。

2. 战术设计

DDD设计思想里领域建模是最核心的一步,该阶段主要目标是提炼和定义出领域模型和之间的关系。

1. 领域建模

建模就是设计的过程,建模的过程就是梳理、走查业务逻辑,拆解为要解决的问题和涉及的业务场景、业务流程、业务概念,在这个过程当中造成对应的领域概念。

若是团队对于业务比较陌生适合采用事件风暴方法进行梳理;若是团队对业务比较熟悉,若是业务流程相对简单,则能够采用四色建模法进行业务梳理。采用这些分析业务的方法能够保证产研团队对业务逻辑的理解在一个水平上。

2. 业务逻辑的显性表达

在完成了实体和值对象的设计后,有的时候会发现有些概念其实在领域上是存在的,但设计和代码里没有Class来体现,可能仅仅是一个基本类型参数加上散落的对该参数的判断检验逻辑,这个时候还须要思考应该把这个概念显性化,定义专门的Class并包含相应逻辑,入出参以相应Class为类型。但凡业务代码逻辑包含了一堆if-else,这时候须要考虑尽量给这段逻辑建模成一个领域概念。

好比CRM系统里判断一条线索是否为推广线索须要看线索的渠道属性是否来自推广平台,那么比较好的方式是这段逻辑用"推广线索"这个概念来显性表达,而不是淹没在代码里不容易理解和维护。

3. 统一语言

为了解决业务逻辑衔接的问题引入了统一语言。每一个业务名词的含义具备明确的定义,产品和研发都统一认识。没有统一语言的沟通严重缺少效率。好比CRM线索的概念,没有统一语言的时候每一个人的理解不同,有的人理解为有过咨询记录的访客是线索,有的人理解为留下过联系方式的访客是线索,有的人理解为有购买意愿的访客是线索等等。

有了统一语言描述,每一个概念就有了明肯定义,能够节省很是大的沟通交流成本。而且这个概念也一样应用在相关的需求文档、设计文档、代码编写中。每一个概念从引入到平常交流,从需求文档到代码实现都有了一致的表达,代码实现和需求描述的真实度高,可理解性和可维护性就变好了。

3. 技术实现

1. 分层架构

为了让代码实现围绕领域模型开展,尽可能下降业务代码和纯技术选型代码的耦合,DDD引入了分层架构。确保了最核心的领域层不依赖其余层,反过来让领域以外的代码依赖领域代码,下降了技术升级带来的影响。

2. DDD框架

框架内定义不一样领域概念须要实现的接口,好比实现了聚合根接口的实体类就成为了聚合的根实体。定义了异常管理规范,不一样的分层应该抛出什么类型的异常。定义了数据访问的资源库接口等等。

3. 领域事件

领域事件是对领域内发生的活动进行的建模,即聚合内的实体状态变化的一个载体。DDD提倡限界上下文间尽可能解耦,尽量使用发布订阅领域事件的协做模式进行上下游解耦。

2.3  DDD vs 数据模型驱动

传统的业务开发模式里,研发受到关系型数据库设计范式、ER图等影响深远,在作软件详细设计过程当中每每先想到如何设计对应的表结构,由此倒推出业务逻辑代码该如何组织。这就是典型的数据模型驱动设计,或者叫面向数据表设计编程。数据模型设计关注的是数据存储,数据尽可能不要冗余,控制表数量不膨胀,更多考虑数据的扩展性,好比新加一个字段尽可能不要在几张表都加,能用一个字段表达就不用两个字段。

这样的思惟跟DDD是相反的,DDD优先考虑领域概念的业务语义表达,具备独立业务概念的东西会尽可能抽象成一个内聚的领域对象。领域对象不只仅有属性,还有该有的行为。

所以,基于数据模型驱动的设计结果每每是:

1. 业务逻辑代码很是过程式,领域实体只包含一堆属性,只是数据表的映射,没有业务行为。也就是常说的只有getter和setter方法的贫血对象。很是缺少领域概念的表达,业务逻辑散乱。好比值对象的设计在DDD里是一个类,在数据模型设计里每每是其余类的几个属性。

2. 聚合是DDD最小的复用单元,粒度更粗。数据模型设计里领域实体的数量跟表数量一一对应,数据表是最小的复用单元,粒度太细。致使业务逻辑对应的实现类须要访问不少的领域实体,实现类之间的调用关系发散而错综复杂。下图是贫血模型和DDD富血模型的区别。

图片

3. 数据表的关系表达很受限,具备主从关系的表之间很难看出主从。在DDD里聚合和聚合内的实体、值对象之间的关系在代码层面有显示的表达。

固然,DDD思想里不是说不用考虑数据表设计,而是要优先考虑领域概念的识别和建模。表设计须要服务于领域模型的设计,是技术实现的细节。所以明白DDD和数据模型驱动设计的区别反过来能更好地理解DDD。

3  实践:案列分析


3.1  业务背景

以爱番番业务中"线索"功能举例,线索管理功能特别多,有建立、清洗、分配、打标签、跟进、回收、退回和转化等十几个管理动做。仅线索建立就分为手工录入建立、文件导入建立、营销系统的后台自动建立、开放平台建立,建立还分为单个建立和批量建立等等。线索这个对象跟其余对象好比客户、商机等联动组合出来不少场景和流程。

3.2  规划阶段

规划阶段须要考虑产品愿景和服务蓝图,须要划分出产品的核心领域,支撑领域,通用领域。若是从0到1开发产品的话规划阶段须要作不少的工做,好比开发一个CRM产品须要考虑产品愿景和服务蓝图,须要聚焦到哪些业务领域,是售前、售中仍是售后?售前还能够细分为营销领域仍是销售领域等等。百度爱番番致力打造易用的、灵活可配的线索管家功能。所以销售领域的线索功能天然是核心模块。须要提供什么线索功能?须要经过分析阶段来拆解。

3.3  分析阶段

分析阶段是基于业务流程和功能分析出具体的业务对象,不一样的业务对象归属划分到限界上下文。由于线索功能复杂,团队对于线索功能认知不一,有必要让相关人员一块儿采用事件风暴方法来分析和梳理业务。事件风暴认为事件流很⼤程度上反映了现实业务逻辑,参与人员基于领域事件发生的时间线,把事件的来龙去脉逐步挖掘出来。整个过程包含识别领域事件、决策命令、领域名词三个步骤。经过尝试回答这几个问题:这个业务涉及的系统产生了什么变化?变化由哪一个角色经过什么方式触发的?系统变化产生了哪些结果?

基于上述步骤,领域专家和相关人员针对线索业务进行事件风暴的结果为:

图片

事件风暴关键图例:

图片

事件风暴实践过程的几点tips:

  1. 事件流几乎等同业务逻辑,以此来推敲业务逻辑的严密性,有果必有因。

  2. 紧扣事件要素:事件、规则、名词、命令、角色。

  3. 命名:紧扣业务,不参杂技术元素,警戒使用泛泛的词汇,尽量地消除命名的⼆义性。

  4. 优先关注happy-path即正常路径,聚焦核心领域里的路径。

  5. 事件风暴不是一蹴而就,保持迭代更新。

基于事件风暴的结果,须要把领域名词和规则等划分到合适的限界上下文。根据前面介绍的如何划分限界上下文的方法,线索相关功能划分为几个限界上下文合适呢?这个时候须要看业务逻辑的复杂程度,还要结合团队规模大小。因为线索功能包含不少业务逻辑,线索归集和建立、线索的分配、线索的跟进等均可以成为一个独立的限界上下文。定义好限界上下文后还须要定义不一样限界上下文的协做关系。通常状况下若是业务容许的状况尽可能选择经过领域事件来协做。根据《领域驱动设计》所述常见的协做关系还包括开放主机服务(即经过暴露接口)、共享内核、防腐层等9种。微服务架构下的限界上下文之间的关系比较常见的有领域事件、开放主机服务、防腐层等。

3.4  设计阶段

设计阶段就是把分析阶段产出的领域名词,领域事件,决策命令用DDD领域概念来承接,并细化每一个领域概念的数据和行为。这也是一种领域建模的过程。

建议的建模过程是:

  1. 业务需求的分析过程自上而下,由业务流程,到用户用例,到领域模型。而设计过程是自下而上的。从领域元素设计开始,最后才是应用服务的编排。

  2. 建议设计优先级是先值对象 → 再实体 → 再聚合 → 再领域服务→ 最后是应用服务,优先考虑领域是否应该为值对象,其次是否为实体,划分出聚合。不属于实体或值对象中的领域行为放到领域服务,须要协调聚合的领域行为设计为领域服务或者应用服务。

  3. 任何业务代码逻辑优先映射到原子性的领域模型,好比值对象、实体、领域事件、资源库接口、外部适配接口,其次再映射到组合性领域模型,好比领域服务、应用服务。

建模过程当中常常会被问到的问题有:

1 值对象能够定义本身的行为吗?

能够,尽量把属于值对象本身的行为放到值对象里。好比联系方式定义成一个值对象,若是它的校验只依赖自身数据,那校验行为应该属于在联系方式这个值对象。

2 聚合该设计为多大粒度?

聚合设计要尽可能小,若是一个实体不是根实体,但同时须要被外界直接访问到,那么这个实体不该该在这个聚合中,应该独立成新的聚合。

3 一个聚合如何访问另一个聚合?

只有聚合根才是访问聚合边界的惟一入口,所以一个聚合须要经过另外一个的聚合的聚合根来访问它,聚合根能够理解为聚合的根实体的Id。

4 应用服务与领域服务的区别?

领域服务处在分层架构的领域层,是领域逻辑的一部分。应用服务处在应用层,负责领域模型的编排。当业务逻辑不属于任何聚合时,应该考虑用领域服务来封装这些逻辑。好比断定订单是否重复,应该属于订单限界上下文的一种业务逻辑,订单聚合自己不能判断是否重复,所以订单判重应该定义为领域服务。

5 应用服务能够直接调用聚合和资源库吗?

能够,可被应用服务编排的对象包括聚合、资源库、领域服务和适配接口。

6 领域事件内容是包含整个聚合里的信息,仍是身份标识信息(订阅方再经过单独接口根据标识进行查询),仍是只包含聚合中一些特定的信息?

领域事件是用于跟其余聚合协做,事件内容不该是整个聚合,而是通过裁剪的特定信息。

根据分析阶段的产出结果,须要把领域名词、规则映射到领域模型。主要几个线索相关领域对象以下图示:

图片

3.5  实现阶段

传统的接口-逻辑-数据访问三层架构里,业务逻辑层的XxxServiceImpl类是个上帝类,每每经过过程式业务逻辑实现。前几行代码作校验,接下来作数据类型转换,而后是业务处理逻辑的代码,中间穿插着经过接口或者dao获取更多的数据;拿到数据后,又是类型转换代码,而后接着一段业务逻辑代码,最后可能还要落库、发布消息等等。这样的代码参杂了太多不一样的代码,很是难以维护。

业界自从DDD的分层架构提出后陆续出现过洋葱架构、六边形架构、整洁架构等,其目标都是为了分离业务和技术,保证领域模型的纯粹性。下图是结合业界架构实践后定制的分层架构,具备如下几个特色:

  1. 接口层负责对外暴露各类协议的接口好比http、tcp,转换成应用服务能认识的协议。

  2. 核心的领域层不依赖其余层,经过资源库包下的接口定义作到依赖倒置,接口参数不能体现具体技术实现细节,领域模型里的实现逻辑只依赖接口。这样作到对领域逻辑的一层防腐。本层里以聚合为单位放置代码,便于之后系统拆分,以聚合为单位。

  3. 应用层定义应用服务,一个接口对应业务场景的一个用例。此外应用层还能够处理横切面事务好比启动数据库事务。

  4. 基础设施层完成资源库的实际实现,以及领域层定义的其余接口的实现如对外部服务的访问,领域事件发布到消息队列中间件等。

  5. 分层架构还定义了每层的项目包结构,不一样的领域概念和数据对象相应的命名规范。

图片

实现阶段常常会被问到的问题有:

1

每层应该用什么类型数据对象承载和传递数据?

如上面分层架构图所示,接口层和应用服务层用DTO对象传递数据,领域层只能见到领域对象即聚合、实体Entity和值对象VO。应用服务层负责把DTO对象转换成领域对象传输到领域层。基础设施层用PO表示数据表,跟领域层调用时须要把PO和领域对象相互作转换。

2

repository和dao的区别?

聚合设计要尽可能小,若是一个实体不是根实体,但同时须要被外界直接访问到,那么这个实体不该该在这个聚合中,应该独立成新的聚合。

3

领域事件的发布应该在领域层仍是应用层?

只要不会破坏各层的依赖顺序,在哪发布都行。取决于领域事件定义在哪层?通常推荐定义在领域层的聚合内。固然即使在应用层发布事件也不会破坏依赖方向。所以聚合、领域服务、应用服务均可以发布事件。

3.6  代码示例

以java代码为例,DDD骨架代码包含了分层架构,每层就是一个maven pom项目,根据用途定义好了多层包结构,每一个领域对象和数据传输对象都有具体的命名方式。基于自研的ddd-framework规范了不一样领域对象须要实现的接口或继承于特定的基类。

总之,尽量作到了能根据需求文档里的业务逻辑很快找到代码所在之处,让不一样的代码待在应该待的分层和包下面。团队成员开玩笑说,如今开发业务代码就像在作填空题,简单直白。

图片

3.7  收益

目前百度爱番番的新服务默认都会在符合DDD架构的骨架代码基础上开发,存量的核心模块也进行过DDD改造。全面实施DDD后产研团队目标更对齐,协做效率更高,收获了不少收益,包括但不限于如下几点:

  1. 产研团队协同成本下降,领域知识获得积累和沉淀。统一语言的使用和维护极大提升了你们对齐的成本。

  2. 业务语义获得显性表达,业务逻辑内聚可复用程度提升,避免了不少散弹式修改和发散式修改。一个需求不用改多个地方,多个需求也不用几个研发集中改同一个地方。

  3. 限界上下文的划分从业务合理性出发,进而微服务的划分会更合理,减小了团队间的耦合和没必要要的协同代价。

  4. 接口数量精简、可控。因为业务代码聚焦领域模型,逻辑内聚,复用性高,急剧减小了接口数量,下降接口维护成本。

  5. 经过预约义好的脚手架建立符合DDD规范的代码骨架,提升了新服务开发的效率。

  6. 代码可读性高,不是代码做者也能快速定位到代码位置,代码设计可以获得传承,可维护性也提升了。

  7. 新人熟悉新业务和新代码的速度极大提升,业务和技术知识的转移代价减低。

3.8  实践总结

从需求到交付的一次典型软件开发流程包括收集提炼需求、需求分析、业务&技术设计、代码实现、测试上线等环节。如何结合软件开发流程,每一个流程阶段具体要作什么、怎么作,特别在编码落地阶段该有什么保障措施?爱番番产研团队在落地过程当中逐步总结出了一套行之有效的DDD实施指南。包括规划、分析、设计到实现四个阶段对应的方法和产出等实施要点。

图片


4  结语:异曲同工、没有银弹


DDD一方面使用分而治之的思想,引入划分领域、限界上下文、模块分层、划分聚合在不一样层次、不一样粒度来下降问题的复杂度。另外一方主张聚焦领域逻辑,经过不一样手段来减小业务和技术的耦合。所以DDD只是大部分软件设计思想一种,软件设计的本质都是为了高内聚低耦合。可是DDD并非万能的,不是全部业务开发场景都适合用DDD。有些简单业务场景不使用DDD反而更恰当。由于DDD有较高的学习门槛,须要整个团队造成统一认识和协同,须要相应的编码规范和架构落地。所以学习和落地DDD时要时刻记住本身的出发点是为了应对如今或者未来的复杂业务领域而来。没必要太拘泥于某些点是否遵照了DDD原则,若是以为用了DDD会比没有用好一点点,也值得迈出这一步。

爱番番产研团队始终秉持“以客户为中心”的理念,运用DDD设计思想构建统一的业务模型,实现业务功能的复用和融合。随着爱番番业务的发展,咱们相信DDD带来的收益会更大。从此咱们会从产品、技术、流程和组织方面持续关注能有效解决软件工程复杂性问题的方法。

本期做者|飞邪

在百度爱番番主要负责销售域和连通域的技术,长期关注技术团队如何高效服务产品团队等研发效能话题,擅长ToB企业级应用的规划和落地。

招聘信息

不管你是后端,前端 ,大数据仍是算法,这里有若干职位在等你,欢迎投递简历, 爱番番业务部期待你的加入!

阅读原文:领域驱动设计(DDD)在百度爱番番的实践

更多干货、内推福利,欢迎关注同名公众号「百度Geek说」~