一文读懂:领域驱动设计DDD

DDD(Domain-Driven Design,领域驱动设计)是一种软件开发方法,它强调软件系统设计应该以问题领域为中心,而不是技术实现为主导。DDD通过一系列手段如统一语言、业务抽象、领域划分和领域建模等来控制软件复杂度,主要用来指导如何解耦业务系统、划分业务模块、定义业务领域模型及其交互。以下是DDD设计的详细解析:

一、DDD设计的核心理念

  1. 领域模型:领域模型是DDD方法的核心,它描述了领域中各个对象和他们之间关系的抽象概念模型。领域模型不仅仅是一个类图或实体关系图,更多的是一种思考方式。

  2. 统一语言:在DDD中,团队成员(包括技术、业务、运营、产品等)使用统一的业务语言进行沟通,这有助于减少误解和冲突。

  3. 限界上下文:为了避免同样的概念或语义在不同的上下文环境中产生歧义,DDD在战略设计上提出了“限界上下文”的概念,用来确定语义所在的领域边界。

  4. 分层架构:DDD架构通常包括领域层(Domain Layer)、应用层(Application Layer)和基础设施层(Infrastructure Layer)等。领域层是核心,负责定义和实现领域模型和业务逻辑;应用层负责协调和组织领域层的操作;基础设施层负责与外部资源的交互。

图片

另外:
推荐一个程序员免费学习的编程网站:我爱编程网(www.love-coding.com
涵盖 Java几乎覆盖了所有主流技术面试题,还有市面上最全的技术精品系列教程,免费提供。
在这里插入图片描述

二、DDD设计的步骤

  1. 需求分析:明确系统的业务需求和功能需求。

  2. 领域分析:进行业务分析,确定业务领域中的“限界上下文”,找到核心业务领域。

  3. 领域建模:基于领域分析的结果,构建领域模型。这包括定义领域对象、建立4领域模型、进行领域建模等方面。

  4. 核心业务逻辑实现:根据领域模型,实现核心业务逻辑。

  5. 技术细节实现:如数据库设计、缓存策略、消息队列等。

三、DDD设计的优势

图片

  1. 提高业务理解能力:DDD强调与业务专家的紧密合作,有助于开发人员深入理解业务需求。
  2. 降低系统复杂度:通过领域划分和领域建模,将复杂的业务系统拆分成若干个相对简单的子领域,从而降低系统复杂度。
  3. 提高代码可维护性:领域模型为代码提供了清晰的业务语义,使得代码更易于理解和维护。
  4. 提高开发效率:通过统一的领域模型和语言,减少了团队成员之间的沟通成本,提高了开发效率。

四、DDD设计的挑战

  1. 需要深入理解业务:DDD要求开发人员对业务有深入的理解,这可能需要较长的时间和努力。
  2. 需要团队协作:DDD强调团队协作和统一语言,需要团队成员之间的紧密配合和沟通。
  3. 建模难度:领域建模是一个复杂的过程,需要开发人员具备较高的抽象能力和建模技巧。

综上所述,DDD设计是一种面向领域的软件开发方法,它通过领域模型、统一语言、限界上下文等核心理念和步骤来指导软件开发过程。DDD设计能够提高业务理解能力、降低系统复杂度、提高代码可维护性和开发效率,但同时也面临一些挑战如深入理解业务、团队协作和建模难度等。

五、落地架构

DDD分层架构、整洁架构、六边形架构都是以领域模型为核心,实行分层架构,内部核心业务逻辑与外部应用、资源隔离并解耦。

1、洋葱架构(整洁架构)

整洁架构最主要的原则是依赖原则,它定义了各层的依赖关系,越往里依赖越低,代码级别越高,越是核心能力。外圆代码依赖只能指向内圆,内圆不需要知道外圆的任何情况。

图片

  • 领域模型实现领域内核心业务逻辑,它封装了企业级的业务规则。领域模型的主体是实体,一个实体可以是一个带方法的对象,也可以是一个数据结构和方法集合。
  • 领域服务实现涉及多个实体的复杂业务逻辑。
  • 应用服务实现与用户操作相关的服务组合与编排,它包含了应用特有的业务流程规则,封装和实现了系统所有用例。
  • 最外层主要提供适配的能力,适配能力分为主动适配和被动适配。主动适配主要实现外部用户、网页、批处理和自动化测试等对内层业务逻辑访问适配。被动适配主要是实现核心业务逻辑对基础资源访问的适配,比如数据库、缓存、文件系统和消息中间件等。
  • 红圈内的领域模型、领域服务和应用服务一起组成软件核心业务能力。

2、六边形架构

图片

  • 红圈内的六边形实现应用的核心业务逻辑;

  • 外六边形完成外部应用、驱动和基础资源等的交互和访问,对前端应用以API主动适配的方式提供服务,对基础资源以依赖倒置被动适配的方式实现资源访问。

3、领域驱动架构

图片

图片

读写分离
问题:事务、消息中间件,
读和写可以使用不同的数据库来实现,读使用更方便读的数据库

项目级微服务

通常项目级微服务之间的集成,发生在微服务的应用层,由应用服务调用其它微服务发布在API网关上的应用服务

图片

企业级中台微服务

BFF(服务于前端的后端,Backend for Frontends)这个词,暂且称它为BFF微服务。BFF微服务与其它微服务存在较大的差异,就是它没有领域模型,因此这个微服务内也不会有领域层。BFF微服务可以承担应用层和用户接口层的主要职能,完成各个中台微服务的服务组合和编排,可以适配不同前端和渠道的要求。

图片

六、代码模型

1. 分层架构

图片

a. 用户接口层(interfaces):面向前端提供服务适配,面向资源层提供资源适配。这一层聚集了接口适配相关的功能。

  • 它主要存放用户接口层与前端交互、展现数据相关的代码。前端应用通过这一层的接口,向应用服务获取展现所需的数据。这一层主要用来处理用户发送的Restful请求,解析用户输入的配置文件,并将数据传递给Application层。数据的组装、数据传输格式以及Facade接口等代码都会放在这一层目录里。

b. 应用层职责(application):实现服务组合和编排,适应业务流程快速变化的需求。这一层聚集了应用服务和事件相关的功能。

  • 它主要存放应用层服务组合和编排相关的代码。应用服务向下基于微服务内的领域服务或外部微服务的应用服务完成服务的编排和组合,向上为用户接口层提供各种应用数据展现支持服务。应用服务和事件等代码会放在这一层目录里。

c. 领域层(domain):实现领域的核心业务逻辑。这一层聚集了领域模型的聚合、聚合根、实体、值对象、领域服务和事件等领域对象,以及它们组合所形成的业务能力。

  • 它主要存放领域层核心业务逻辑相关的代码。领域层可以包含多个聚合代码包,它们共同实现领域模型的核心业务逻辑。聚合以及聚合内的实体、方法、领域服务和事件等代码会放在这一层目录里。

d. 基础层(infrastructure):贯穿所有层,为各层提供基础资源服务。这一层聚集了各种底层资源相关的服务和能力。

  • 它主要存放基础资源服务相关的代码,为其它各层提供的通用技术能力、三方软件包、数据库服务、配置和基础资源服务的代码都会放在这一层目录里。

2. 代码目录结构

图片

  • Interfaces(用户接口层):它主要存放用户接口层与前端交互、展现数据相关的代码。前端应用通过这一层的接口,向应用服务获取展现所需的数据。这一层主要用来处理用户发送的Restful请求,解析用户输入的配置文件,并将数据传递给Application层。数据的组装、数据传输格式以及Facade接口等代码都会放在这一层目录里。

  • Application(应用层):它主要存放应用层服务组合和编排相关的代码。应用服务向下基于微服务内的领域服务或外部微服务的应用服务完成服务的编排和组合,向上为用户接口层提供各种应用数据展现支持服务。应用服务和事件等代码会放在这一层目录里。

  • Domain(领域层):它主要存放领域层核心业务逻辑相关的代码。领域层可以包含多个聚合代码包,它们共同实现领域模型的核心业务逻辑。聚合以及聚合内的实体、方法、领域服务和事件等代码会放在这一层目录里。

    • Infrastructure(基础层):它主要存放基础资源服务相关的代码,为其它各层提供的通用技术能力、三方软件包、数据库服务、配置和基础资源服务的代码都会放在这一层目录里。
  • 3. 边界

  • 图片

逻辑边界:微服务内聚合之间的边界是逻辑边界。它是一个虚拟的边界,强调业务的内聚,可根据需要变成物理边界,也就是说聚合也可以独立为微服务。

物理边界:微服务之间的边界是物理边界。它强调微服务部署和运行的隔离,关注微服务的服务调用、容错和运行等。

代码边界:不同层或者聚合之间代码目录的边界是代码边界。它强调的是代码之间的隔离,方便架构演进时代码的重组。

通过以上边界,我们可以让业务能力高内聚、代码松耦合,且清晰的边界,可以快速实现微服务代码的拆分和组合,轻松实现微服务架构演进。但有一点一定要格外注意,边界清晰的微服务,不是大单体向小单体的演进。

注意:

第一点:聚合之间的代码边界一定要清晰。聚合之间的服务调用和数据关联应该是尽可能的松耦合和低关联,聚合之间的服务调用应该通过上层的应用层组合实现调用,原则上不允许聚合之间直接调用领域服务。这种松耦合的代码关联,在以后业务发展和需求变更时,可以很方便地实现业务功能和聚合代码的重组,在微服务架构演进中将会起到非常重要的作用。

第二点:你一定要有代码分层的概念。写代码时一定要搞清楚代码的职责,将它放在职责对应的代码目录内。应用层代码主要完成服务组合和编排,以及聚合之间的协作,它是很薄的一层,不应该有核心领域逻辑代码。领域层是业务的核心,领域模型的核心逻辑代码一定要在领域层实现。如果将核心领域逻辑代码放到应用层,你的基于DDD分层架构模型的微服务慢慢就会演变成传统的三层架构模型了。

4. 跨服务调用

在严格分层架构模式下,不允许服务的跨层调用,每个服务只能调用它的下一层服务。服务从下到上依次为:实体方法、领域服务和应用服务。

如果需要实现服务的跨层调用,我们应该怎么办?我建议你采用服务逐层封装的方式。

图片

1、实体方法的封装

实体方法是最底层的原子业务逻辑。如果单一实体的方法需要被跨层调用,你可以将它封装成领域服务,这样封装的领域服务就可以被应用服务调用和编排了。如果它还需要被用户接口层调用,你还需要将这个领域服务封装成应用服务。经过逐层服务封装,实体方法就可以暴露给上面不同的层,实现跨层调用。

封装时服务前面的名字可以保持一致,你可以用DomainService或AppService后缀来区分领域服务或应用服务。

2、领域服务的组合和封装

领域服务会对多个实体和实体方法进行组合和编排,供应用服务调用。如果它需要暴露给用户接口层,领域服务就需要封装成应用服务。

3.应用服务的组合和编排

应用服务会对多个领域服务进行组合和编排,暴露给用户接口层,供前端应用调用。

在应用服务组合和编排时,你需要关注一个现象:多个应用服务可能会对多个同样的领域服务重复进行同样业务逻辑的组合和编排。当出现这种情况时,你就需要分析是不是领域服务可以整合了。你可以将这几个不断重复组合的领域服务,合并到一个领域服务中实现。这样既省去了应用服务的反复编排,也实现了服务的演进。这样领域模型将会越来越精炼,更能适应业务的要求。

应用服务类放在应用层Service目录结构下。领域事件的发布和订阅类放在应用层Event目录结构下。

5. 调用流程

图片

1)微服务内跨层服务调用

微服务架构下往往采用前后端分离的设计模式,前端应用独立部署。前端应用调用发布在API网关上的 Facade 服务,Facade 定向到应用服务。应用服务作为服务组织和编排者,它的服务调用有这样两种路径:

应用服务调用并组装领域服务。此时领域服务会组装实体和实体方法,实现核心领域逻辑。领域服务通过仓储服务获取持久化数据对象,完成实体数据初始化。

应用服务直接调用仓储服务。这种方式主要针对像缓存、文件等类型的基础层数据访问。这类数据主要是查询操作,没有太多的领域逻辑,不经过领域层,不涉及数据库持久化对象。

2)微服务之间的服务调用

微服务之间的应用服务可以直接访问,也可以通过API网关访问。由于跨微服务操作,在进行数据新增和修改操作时,你需关注分布式事务,保证数据的一致性。

3)领域事件驱动

领域事件驱动包括微服务内和微服务之间的事件。微服务内通过事件总线(EventBus)完成聚合之间的异步处理。微服务之间通过消息中间件完成。异步化的领域事件驱动机制是一种间接的服务访问方式。

当应用服务业务逻辑处理完成后,如果发生领域事件,可调用事件发布服务,完成事件发布。

当接收到订阅的主题数据时,事件订阅服务会调用事件处理领域服务,完成进一步的业务操作。

6. 服务的封装与组合

图片

1)基础层

基础层的服务形态主要是仓储服务。仓储服务包括接口和实现两部分。仓储接口服务供应用层或者领域层服务调用,仓储实现服务,完成领域对象的持久化或数据初始化。

2)领域层

领域层实现核心业务逻辑,负责表达领域模型业务概念、业务状态和业务规则。主要的服务形态有实体方法和领域服务。

实体采用充血模型,在实体类内部实现实体相关的所有业务逻辑,实现的形式是实体类中的方法。实体是微服务的原子业务逻辑单元。在设计时我们主要考虑实体自身的属性和业务行为,实现领域模型的核心基础能力。不必过多考虑外部操作和业务流程,这样才能保证领域模型的稳定性。

DDD提倡富领域模型,尽量将业务逻辑归属到实体对象上,实在无法归属的部分则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,实现跨多个实体的复杂核心业务逻辑。

对于严格分层架构,如果单个实体的方法需要对应用层暴露,则需要通过领域服务封装后才能暴露给应用服务。

3)应用层

应用层用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,负责不同聚合之间的服务和数据协调,负责微服务之间的事件发布和订阅。

通过应用服务对外暴露微服务的内部功能,这样就可以隐藏领域层核心业务逻辑的复杂性以及内部实现机制。应用层的主要服务形态有:应用服务、事件发布和订阅服务。

应用服务内用于组合和编排的服务,主要来源于领域服务,也可以是外部微服务的应用服务。除了完成服务的组合和编排外,应用服务内还可以完成安全认证、权限校验、初步的数据校验和分布式事务控制等功能。

为了实现微服务内聚合之间的解耦,聚合之间的服务调用和数据交互应通过应用服务来完成。原则上我们应该禁止聚合之间的领域服务直接调用和聚合之间的数据表关联。

4)用户接口层

用户接口层是前端应用和微服务之间服务访问和数据交换的桥梁。它处理前端发送的Restful请求和解析用户输入的配置文件等,将数据传递给应用层。或获取应用服务的数据后,进行数据组装,向前端提供数据服务。主要服务形态是Facade服务。

Facade服务分为接口和实现两个部分。完成服务定向,DO与DTO数据的转换和组装,实现前端与应用层数据的转换和交换。

图片

7. 服务依赖关系

1)松散分层架构

松散分层架构中,领域层的实体方法和领域服务可以直接暴露给应用层和用户接口层。松散分层架构的服务依赖关系,无需逐级封装,可以快速暴露给上层。

但它存在一些问题,第一个是容易暴露领域层核心业务的实现逻辑;第二个是当实体方法或领域服务发生服务变更时,由于服务同时被多层服务调用和组合,不容易找出哪些上层服务调用和组合了它,不方便通知到所有的服务调用方。

图片

2)严格分层架构

严格分层架构中,每一层服务只能向紧邻的上一层提供服务。虽然实体、实体方法和领域服务都在领域层,但实体和实体方法只能暴露给领域服务,领域服务只能暴露给应用服务。

在严格分层架构中,服务如果需要跨层调用,下层服务需要在上层封装后,才可以提供跨层服务。比如实体方法需要向应用服务提供服务,它需要封装成领域服务。

这是因为通过封装你可以避免将核心业务逻辑的实现暴露给外部,将实体和方法封装成领域服务,也可以避免在应用层沉淀过多的本该属于领域层的核心业务逻辑,避免应用层变得臃肿。还有就是当服务发生变更时,由于服务只被紧邻上层的服务调用和组合,你只需要逐级告知紧邻上层就可以了,服务可管理性比松散分层架构要好是一定的。

图片

8. 数据对象转换

  • 数据持久化对象PO(Persistent Object),与数据库结构一一映射,是数据持久化过程中的数据载体。
  • 领域对象DO(Domain Object),微服务运行时的实体,是核心业务的载体。
  • 数据传输对象DTO(Data Transfer Object),用于前端与应用层或者微服务之间的数据组装和传输,是应用之间数据传输的载体。视图对象VO(View Object),用于封装展示层指定页面或组件的数据。

图片

七、总结

1. DDD使用的误区

很多人在接触微服务后,但凡是系统,一概都想设计成微服务架构。其实有些业务场景,单体架构的开发成本会更低,开发效率更高,采用单体架构也不失为好的选择。同样,虽然DDD很好,但有些传统设计方法在微服务设计时依然有它的用武之地。下面我们就来聊聊DDD使用的几个误区。

  1. 很多人在学会DDD后,可能会将其用在所有业务域,即全部使用DDD来设计。DDD从战略设计到战术设计,是一个相对复杂的过程,首先企业内要培养DDD的文化,其次对团队成员的设计和技术能力要求相对比较高。在资源有限的情况下,应聚焦核心域,建议你先从富领域模型的核心域开始,而不必一下就在全业务域推开。

  2. 部采用DDD战术设计方法,不同的设计方法有它的适用环境,我们应选择它最擅长的场景。DDD有很多的概念和战术设计方法,比如聚合根和值对象等。聚合根利用仓储管理聚合内实体数据之间的一致性,这种方法对于管理新建和修改数据非常有效,比如在修改订单数据时,它可以保证订单总金额与所有商品明细金额的一致,但它并不擅长较大数据量的查询处理,甚至有延迟加载进而影响效率的问题。

而传统的设计方法,可能一条简单的SQL语句就可以很快地解决问题。而很多贫领域模型的业务,比如数据统计和分析,DDD很多方法可能都用不上,或用得并不顺手,而传统的方法很容易就解决了。

因此,在遵守领域边界和微服务分层等大原则下,在进行战术层面设计时,我们应该选择最适合的方法,不只是DDD设计方法,当然还应该包括传统的设计方法。这里要以快速、高效解决实际问题为最佳,不要为做DDD而做DDD。

  1. 重战术设计而轻战略设计
    很多DDD初学者,学习DDD的主要目的,可能是为了开发微服务,因此更看重DDD的战术设计实现。殊不知DDD是一种从领域建模到微服务落地的全方位的解决方案。

战略设计时构建的领域模型,是微服务设计和开发的输入,它确定了微服务的边界、聚合、代码对象以及服务等关键领域对象。领域模型边界划分得清不清晰,领域对象定义得明不明确,会决定微服务的设计和开发质量。没有领域模型的输入,基于DDD的微服务的设计和开发将无从谈起。因此我们不仅要重视战术设计,更要重视战略设计。

  1. DDD只适用于微服务
    DDD是在微服务出现后才真正火爆起来的,很多人会认为DDD只适用于微服务。在DDD沉默的二十多年里,其实它一直也被应用在单体应用的设计中。

具体项目实施时,要吸取DDD的核心设计思想和理念,结合具体的业务场景和团队技术特点,多种方法组合,灵活运用,用正确的方式解决实际问题。

2. 设计原则

DDD基于各种考虑,有很多的设计原则,也用到了很多的设计模式。条条框框多了,很多人可能就会被束缚住,总是担心或犹豫这是不是原汁原味的DDD。其实我们不必追求极致的DDD,这样做反而会导致过度设计,增加开发复杂度和项目成本。

DDD的设计原则或模式,是考虑了很多具体场景或者前提的。有的是为了解耦,如仓储服务、边界以及分层,有的则是为了保证数据一致性,如聚合根管理等。在理解了这些设计原则的根本原因后,有些场景你就可以灵活把握设计方法了,你可以突破一些原则,不必受限于条条框框,大胆选择最合适的方法。

领域事件采用消息异步机制,发布方和订阅方数据如何保证一致性?微服务内聚合之间领域事件是否一定要用事件总线?

在领域事件设计中,为了解耦微服务,微服务之间数据采用最终一致性原则。由于发布方是在消息总线发布消息以后,并不关心数据是否送达,或者送达后订阅方是否正常处理,因此有些技术人会担心发布方和订阅方数据一致性的问题。

那在对数据一致性要求比较高的业务场景,我们是有相关的设计考虑的。也就是发送方和订阅方的事件数据都必须落库,发送方除了保存业务数据以外,在往消息中间件发布消息之前,会先将要发布的消息写入本地库。而接收方在处理消息之前,需要先将收到的消息写入本地库。然后可以采用定期对发布方和订阅方的事件数据对账的操作,识别出不一致的数据。如果数据出现异常或不一致的情况,可以启动定时程序再次发送,必要时可以转人工操作处理。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

首席架构师专栏

喜欢请点赞,么么哒

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值