# 领域驱动设计
# 1、DDD概述
DDD是什么:一种架构设计方法论,通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现架构演进。试图分离技术实现的复杂度,使业务从复杂变得简单,更容易让人理解,技术实现更容易。
DDD核心思想:通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性。核心思想就是将问题域逐步分解,降低业务理解和系统实现的复杂度。
DDD的好处:利用 DDD 设计方法来建立领域模型,划分领域边界,再根据这些领域边界从业务视角来划分微服务边界。而按照 DDD 方法设计出的微服务的业务和应用边界都非常合理,可以很好地实现微服务内部和外部的“高内聚、低耦合”。
# 2、相关概念
DDD 包括战略设计和战术设计两部分,即模型的建立(战略设计)和模型的实现(战术设计)
# 1、战略设计
从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
相关概念:
- 领域:某个界限内的业务问题域
- 子域
- 核心域:决定产品的公司核心竞争力的子域
- 通用域:同时被多个子域使用的通用功能子域,比如认证、权限等
- 支撑域: 既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域
- 子域
- 通用语言:在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。
- 界限上下文(BC bounded context):定义领域的边界,用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。
解析:子域与界限上下文
做子域划分的主要目的就是把问题域细分,然后你才能做事件风暴,子域太大你hold不住!这些细分的子域是相对较大的领域,在事件风暴之前先做子域细分。子域细分后,你就可以在子域内用事件风暴建立领域模型,建模过程中会对聚合进行归类,形成限界上下文边界。这个限界上下文,其实你也可以认为是一个子域。
如何实现:
分两步:捕获行为需求(通常采用事件风暴);领域建模(一般使用UML类图表示的领域模型图)
- 事件风暴的主要过程:
- 这里的第一步是识别领域事件,在这一步,我们要找到业务流程中发生了哪些事情;
- 第二步是识别命令,进一步说明是什么角色,做了什么操作,导致上述事情的发生;
- 而第三步是识别领域名词,从领域事件和命令中找到名词性概念,为进一步的领域建模打下基础。
- 领域建模
- 建立领域模型,主要是要识别领域对象(domain object),领域对象之间的关系,以及领域对象的关键属性,必要的时候还要将领域对象组织成模块。
- 领域建模主要有两个目的:将知识可视化,准确、深刻地反映领域知识,并且在业务和技术人员之间达成一致;指导系统的设计和编码,也就是说,领域模型应该能够比较容易地转化成数据库模式和代码实现。
- 领域模型是用领域模型图来表达的,通常用 UML 来画。
# 2、战术设计
战术设计:从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现
相关概念:
- 实体(Entity):实体一般对应业务对象,它具有业务属性和业务行为;DO领域对象,通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现
- 值对象(ValueObject):值对象主要是属性集合,对实体的状态和特征进行描述。说白了就是一些属性的集合类,实体对象中进行引用
- 聚合(Aggregate):聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
- 聚合根(AggregateRoot):聚合有一个聚合根和上下文边界,如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。
解析:领域模型中对象的层次从内到外依次是:值对象、实体、聚合和限界上下文。
DDD 领域建模通常采用事件风暴,它通常采用用例分析、场景分析和用户旅程分析等方法,通过头脑风暴列出所有可能的业务行为和事件,然后找出产生这些行为的领域对象,并梳理领域对象之间的关系,找出聚合根,找出与聚合根业务紧密关联的实体和值对象,再将聚合根、实体和值对象组合,构建聚合。
在事件风暴中,我们会根据一些业务操作和行为找出实体(Entity)或值对象(ValueObject),进而将业务关联紧密的实体和值对象进行组合,构成聚合,再根据业务语义将多个聚合划定到同一个限界上下文(Bounded Context)中,并在限界上下文内完成领域建模。
- 领域事件(Domain Event):在事件风暴(Event Storming)时,我们发现除了命令和操作等业务行为以外,还有一种非常重要的事件,这种事件发生后通常会导致进一步的业务操作,在 DDD 中这种事件被称为领域事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。
- 领域服务:领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。其次,你要知道,实体和领域服务在实现业务逻辑上不是同级的,当领域中的某些功能,单一实体(或者值对象)不能实现时,领域服务就会出马,它可以组合聚合内的多个实体(或者值对象),实现复杂的业务逻辑。
如何实现:
- 数据库设计/分层设计/代码实现
- 聚合和聚合根
员工和技能这两个实体有两个重要特征:第一,具有整体与部分的关系。第二,具有不变规则,而且这种不变规则在并发的时候可能被破坏。具有这样特征的一组领域对象,在 DDD 里就叫做一个聚合(Aggregate)。我们把员工、技能和工作经验的这种聚合关系,在领域模型中显式地表达出来,如下图:
在员工实体名字上方,我们加了一个 '< < aggregate root > >' 的标识,中文是聚合根的意思。在一个聚合里,像员工这样代表整体的实体就是聚合根。一个聚合只有一个聚合根。
# 3、分层架构
# 1、DDD的分层架构
分层架构有效的降低层与层之间的依赖,其核心理念都是为了设计出“高内聚低耦合”的架构,轻松实现架构演进。
1、DDD分层架构的演进
DDD 的分层架构在不断发展。最早是传统的四层架构;后来四层架构有了进一步的优化,实现了各层对基础层的解耦;再后来领域层和应用层之间增加了上下文环境(Context)层,五层架构(DCI)就此形成了。
- 传统四层架构中,基础层是被其它层依赖的,它位于最核心的位置,那按照分层架构的思想,它应该就是核心,但实际上领域层才是软件的核心,所以这种依赖是有问题的
- 后来我们采用了依赖倒置(Dependency inversion principle,DIP)的设计,优化了传统的四层架构,实现了各层对基础层的解耦。
2、DDD四层架构的详解
- 用户接口层:负责向用户显示信息和解释用户指令
- 应用层:理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。
- 应用服务是在应用层的,它负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,以粗粒度的服务通过 API 网关向前端发布。还有,应用服务还可以进行安全认证、权限校验、事务控制、发送或订阅领域事件等。
- 在设计和开发时,不要将本该放在领域层的业务逻辑放到应用层中实现。因为庞大的应用层会使领域模型失焦,时间一长你的微服务就会演化为传统的三层架构,业务逻辑会变得混乱。
- 领域层:作用是实现企业核心业务逻辑,通过各种校验手段保证业务的正确性。领域层主要体现领域模型的业务能力,它用来表达业务概念、业务状态和业务规则。包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。
- 基础层:基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。
- 基础层包含基础服务,它采用依赖倒置设计,封装基础资源服务,实现应用层、领域层与基础层的解耦,降低外部资源变化对应用的影响。
解析:重要的原则为每层只能与位于其下方的层发生耦合。建议使用严格分层架构,领域服务只能被应用服务调用,而应用服务只能被用户接口层调用,服务是逐层对外封装或组合的,依赖关系清晰。
3、三层架构演进到DDD分层架构
三层架构向 DDD 分层架构演进,主要发生在业务逻辑层和数据访问层。
- DDD 分层架构在用户接口层引入了 DTO,给前端提供了更多的可使用数据和更高的展示灵活性
- DDD 分层架构对三层架构的业务逻辑层进行了更清晰的划分,改善了三层架构核心业务逻辑混乱,代码改动相互影响大的情况。DDD 分层架构将业务逻辑层的服务拆分到了应用层和领域层。应用层快速响应前端的变化,领域层实现领域模型的能力。
- 三层架构数据访问采用 DAO 方式;DDD 分层架构的数据库等基础资源访问,采用了仓储(Repository)设计模式,通过依赖倒置实现各层对基础资源的解耦。仓储又分为两部分:仓储接口和仓储实现。仓储接口放在领域层中,仓储实现放在基础层。原来三层架构通用的第三方工具包、驱动、Common、Utility、Config 等通用的公共的资源类统一放到了基础层。
# 2、微服务的几种架构
DDD 分层架构、整洁架构、六边形架构都是以领域模型为核心,实行分层架构,内部核心业务逻辑与外部应用、资源隔离并解耦。
1、整洁架构(洋葱架构)
在整洁架构里,同心圆代表应用软件的不同部分,从里到外依次是领域模型、领域服务、应用服务和最外围的容易变化的内容,比如用户界面和基础设施。整洁架构最主要的原则是依赖原则,它定义了各层的依赖关系,越往里依赖越低,代码级别越高,越是核心能力。外圆代码依赖只能指向内圆,内圆不需要知道外圆的任何情况。
在洋葱架构中,各层的职能是这样划分的:
- 领域模型实现领域内核心业务逻辑,它封装了企业级的业务规则。领域模型的主体是实体,一个实体可以是一个带方法的对象,也可以是一个数据结构和方法集合。
- 领域服务实现涉及多个实体的复杂业务逻辑。
- 应用服务实现与用户操作相关的服务组合与编排,它包含了应用特有的业务流程规则,封装和实现了系统所有用例。
- 最外层主要提供适配的能力,适配能力分为主动适配和被动适配。主动适配主要实现外部用户、网页、批处理和自动化测试等对内层业务逻辑访问适配。被动适配主要是实现核心业务逻辑对基础资源访问的适配,比如数据库、缓存、文件系统和消息中间件等。
- 红圈内的领域模型、领域服务和应用服务一起组成软件核心业务能力。
2、六边形架构(端口适配器架构)
追溯微服务架构的渊源,一般都会涉及到六边形架构。六边形架构的核心理念是:应用是通过端口与外部进行交互的。
在下图的六边形架构中,红圈内的核心业务逻辑(应用程序和领域模型)与外部资源(包括 APP、Web 应用以及数据库资源等)完全隔离,仅通过适配器进行交互。它解决了业务逻辑与用户界面的代码交错问题,很好地实现了前后端分离。六边形架构各层的依赖关系与整洁架构一样,都是由外向内依赖。
六边形架构将系统分为内六边形和外六边形两层,这两层的职能划分如下:
- 红圈内的六边形实现应用的核心业务逻辑;
- 外六边形完成外部应用、驱动和基础资源等的交互和访问,对前端应用以 API 主动适配的方式提供服务,对基础资源以依赖倒置被动适配的方式实现资源访问。
六边形架构的一个端口可能对应多个外部系统,不同的外部系统也可能会使用不同的适配器,由适配器负责协议转换。这就使得应用程序能够以一致的方式被用户、程序、自动化测试和批处理脚本使用。
3、三种架构的比较
虽然 DDD 分层架构、整洁架构、六边形架构的架构模型表现形式不一样,但你不要被它们的表象所迷惑,这三种架构模型的设计思想正是微服务架构高内聚低耦合原则的完美体现,而它们身上闪耀的正是以领域模型为中心的设计思想。
图中的红色线框,它们是非常重要的分界线,这三种架构里面都有,它的作用就是将核心业务逻辑与外部应用、基础资源进行隔离。红色框内部主要实现核心业务逻辑,但核心业务逻辑也是有差异的,有的业务逻辑属于领域模型的能力,有的则属于面向用户的用例和流程编排能力。按照这种功能的差异,我们在这三种架构中划分了应用层和领域层,来承担不同的业务逻辑。
- 领域层实现面向领域模型,实现领域模型的核心业务逻辑,属于原子模型,它需要保持领域模型和业务逻辑的稳定,对外提供稳定的细粒度的领域服务,所以它处于架构的核心位置。
- 应用层实现面向用户操作相关的用例和流程,对外提供粗粒度的 API 服务。它就像一个齿轮一样进行前台应用和领域层的适配,接收前台需求,随时做出响应和调整,尽量避免将前台需求传导到领域层。应用层作为配速齿轮则位于前台应用和领域层之间。
这三种架构都考虑了前端需求的变与领域模型的不变。需求变幻无穷,但变化总是有矩可循的,用户体验、操作习惯、市场环境以及管理流程的变化,往往会导致界面逻辑和流程的多变。但总体来说,不管前端如何变化,在企业没有大的变革的情况下,核心领域逻辑基本不会大变,所以领域模型相对稳定,而用例和流程则会随着外部应用需求而随时调整。把握好这个规律,我们就知道该如何设计应用层和领域层了。
架构模型通过分层的方式来控制需求变化从外到里对系统的影响,从外向里受需求影响逐步减小。面向用户的前端可以快速响应外部需求进行调整和发布,灵活多变,应用层通过服务组合和编排来实现业务流程的快速适配上线,减少传导到领域层的需求,使领域层保持长期稳定。这样设计的好处很明显了,就是可以保证领域层的核心业务逻辑不会因为外部需求和流程的变动而调整,对于建立前台灵活、中台稳固的架构很有帮助。
# 4、DDD的几种实操
# 1、用DDD重构业务模型
1、自顶向下的策略
自顶向下的策略适用于全新的应用系统建设,或旧系统推倒重建的情况。
由于这种策略不必受限于现有系统,你可以用 DDD 领域逐级分解的领域建模方法。从下面这张图我们可以看出它的主要步骤:
- 第一步是将领域分解为子域,子域可以分为核心域、通用域和支撑域;
- 第二步是对子域建模,划分领域边界,建立领域模型和限界上下文;
- 第三步则是根据限界上下文进行微服务设计。
2、自底向上的策略
自底向上策略适用于遗留系统业务模型的演进式重构。
基于业务和系统现状完成领域建模。首先分别完成系统所在业务域的领域建模;然后对齐业务域,找出具有同类或相似业务功能的领域模型,对比分析领域模型的差异,重组领域对象,重构领域模型。这个过程会沉淀公共和复用的业务能力,会将分散的业务模型整合。
# 2、事件风暴
事件风暴正是 DDD 战略设计中经常使用的一种方法,它可以快速分析和分解复杂的业务领域,完成领域建模。
事件风暴是一项团队活动,领域专家与项目团队通过头脑风暴的形式,罗列出领域中所有的领域事件,整合之后形成最终的领域事件集合,然后对每一个事件,标注出导致该事件的命令,再为每一个事件标注出命令发起方的角色。命令可以是用户发起,也可以是第三方系统调用或者定时器触发等,最后对事件进行分类,整理出实体、聚合、聚合根以及限界上下文。
领域建模的过程主要包括产品愿景、业务场景分析、领域建模和微服务拆分与设计这几个重要阶段。
1、产品愿景
产品愿景的主要目的是对产品顶层价值的设计,使产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。需要思考:它的业务范围、目标用户、核心价值和愿景,与其它同类产品的差异和优势在哪里?
参考示例如下:
2、业务场景分析
场景分析是从用户视角出发的,根据业务流程或用户旅程,采用用例和场景分析,探索领域中的典型场景,找出领域事件、实体和命令等领域对象,支撑领域建模。事件风暴参与者要尽可能地遍历所有业务细节,充分发表意见,不要遗漏业务要点。
场景分析时会产生很多的命令和领域事件。用蓝色来表示命令,用橙色表示领域事件,用黄色表示补充信息,比如用户信息数据来源于 HR 系统的说明参考如下:
3、领域建模
领域建模时,我们会根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。领域模型利用限界上下文向上可以指导微服务设计,通过聚合向下可以指导聚合根、实体和值对象的设计。
- 从命令和事件中提取产生这些行为的实体,用绿色贴纸表示实体。通过分析用户中台的命令和事件等行为数据,提取了产生这些行为的用户、账户、认证票据、系统、菜单、岗位和用户日志七个实体。
- 根据聚合根的管理性质从七个实体中找出聚合根,比如,用户管理用户相关实体以及值对象,系统可以管理与系统相关的菜单等实体等,可以找出用户和系统等聚合根。然后根据业务依赖和业务内聚原则,将聚合根以及它关联的实体和值对象组合为聚合,比如系统和菜单实体可以组合为“系统功能”聚合。按照上述方法,用户中台就有了系统功能、岗位、用户信息、用户日志、账户和认证票据六个聚合。
- 划定限界上下文,根据上下文语义将聚合归类。根据用户域的上下文语境,用户基本信息和用户日志信息这两个聚合共同构成用户信息域,分别管理用户基本信息、用户登录和操作日志。认证票据和账户这两个聚合共同构成认证域,分别实现不同方式的登录和认证。系统功能和岗位这两个聚合共同构成权限域,分别实现系统和菜单管理以及系统的岗位配置。根据业务边界,我们可以将用户中台划分为三个限界上下文:用户信息、认证和权限。
到这里我们就完成了用户中台领域模型的构建了。那由于领域建模的过程中产生的领域对象实在太多了,我们可以借助表格来记录。
4、微服务拆分与设计
在微服务拆分与设计时,我们不能简单地将领域模型作为拆分微服务的唯一标准,它只能作为微服务拆分的一个重要依据。
用户中台微服务设计如果不考虑非业务因素,我们完全可以按照领域模型与微服务一对一的关系来设计,将用户中台设计为:用户、认证和权限三个微服务。但如果用户日志数据量巨大,大到需要采用大数据技术来实现,这时用户信息聚合与用户日志聚合就会有技术异构。虽然在领域建模时,我们将他们放在一个了领域模型内,但如果考虑技术异构,这两个聚合就不适合放到同一个微服务里了。我们可以以聚合作为拆分单位,将用户基本信息管理和用户日志管理拆分为两个技术异构的微服务,分别用不同的技术来实现它们。
# 3、代码模型
1、代码结构
- Interfaces(用户接口层):它主要存放用户接口层与前端交互、展现数据相关的代码。前端应用通过这一层的接口,向应用服务获取展现所需的数据。这一层主要用来处理用户发送的 Restful 请求,解析用户输入的配置文件,并将数据传递给 Application 层。数据的组装、数据传输格式以及 Facade 接口等代码都会放在这一层目录里。
- Assembler:实现 DTO 与领域对象之间的相互转换和数据交换。一般来说 Assembler 与 DTO 总是一同出现。
- Dto:它是数据传输的载体,内部不存在任何业务逻辑,我们可以通过 DTO 把内部的领域对象与外界隔离。
- Facade:提供较粗粒度的调用接口,将用户请求委派给一个或多个应用服务进行处理。
- Application(应用层):它主要存放应用层服务组合和编排相关的代码。应用服务向下基于微服务内的领域服务或外部微服务的应用服务完成服务的编排和组合,向上为用户接口层提供各种应用数据展现支持服务。应用服务和事件等代码会放在这一层目录里。
- Event(事件):这层目录主要存放事件相关的代码。它包括两个子目录:publish 和 subscribe。前者主要存放事件发布相关代码,后者主要存放事件订阅相关代码(事件处理相关的核心业务逻辑在领域层实现)。
- Service(应用服务):这层的服务是应用服务。应用服务会对多个领域服务或外部应用服务进行封装、编排和组合,对外提供粗粒度的服务。应用服务主要实现服务组合和编排,是一段独立的业务逻辑。你可以将所有应用服务放在一个应用服务类里,也可以把一个应用服务设计为一个应用服务类,以防应用服务类代码量过大。
- Domain(领域层):它主要存放领域层核心业务逻辑相关的代码。领域层可以包含多个聚合代码包,它们共同实现领域模型的核心业务逻辑。聚合以及聚合内的实体、方法、领域服务和事件等代码会放在这一层目录里。
- Aggregate(聚合):它是聚合软件包的根目录,可以根据实际项目的聚合名称命名,比如权限聚合。在聚合内定义聚合根、实体和值对象以及领域服务之间的关系和边界。聚合内实现高内聚的业务逻辑,它的代码可以独立拆分为微服务。
- Entity(实体):它存放聚合根、实体、值对象以及工厂模式(Factory)相关代码。实体类采用充血模型,同一实体相关的业务逻辑都在实体类代码中实现。跨实体的业务逻辑代码在领域服务中实现。
- Event(事件):它存放事件实体以及与事件活动相关的业务逻辑代码。
- Service(领域服务):它存放领域服务代码。一个领域服务是多个实体组合出来的一段业务逻辑。你可以将聚合内所有领域服务都放在一个领域服务类中,你也可以把每一个领域服务设计为一个类。如果领域服务内的业务逻辑相对复杂,我建议你将一个领域服务设计为一个领域服务类,避免由于所有领域服务代码都放在一个领域服务类中,而出现代码臃肿的问题。领域服务封装多个实体或方法后向上层提供应用服务调用。
- Repository(仓储):它存放所在聚合的查询或持久化领域对象的代码,通常包括仓储接口和仓储实现方法。为了方便聚合的拆分和组合,我们设定了一个原则:一个聚合对应一个仓储。
按照 DDD 分层架构,仓储实现本应该属于基础层代码,但为了在微服务架构演进时,保证代码拆分和重组的便利性,我是把聚合仓储实现的代码放到了聚合包内。这样,如果需求或者设计发生变化导致聚合需要拆分或重组时,我们就可以将包括核心业务逻辑和仓储代码的聚合包整体迁移,轻松实现微服务架构演进。
- Infrastructure(基础层):它主要存放基础资源服务相关的代码,为其它各层提供的通用技术能力、三方软件包、数据库服务、配置和基础资源服务的代码都会放在这一层目录里。
- Config:主要存放配置相关代码。
- Util:主要存放平台、开发框架、消息、数据库、缓存、文件、总线、网关、第三方类库、通用算法等基础代码,你可以为不同的资源类别建立不同的子目录。
2、领域模型与代码模型的一致性