记录些 DDD 实践规范(9)

问题1:DDD的外部接口调用,应该放在哪一层?

无论是RPC (如Feign),还是HTTPClient调用Rest API,具体的外部接口调用实现放在infrastructure 基础设施层。

在领域驱动设计(DDD)中,基础设施层负责处理与外部资源的交互,包括数据库、文件系统、消息队列、外部服务等。

来看看 COLA 的 DDD 架构,具体如下图

图片

COLA(Clean Object-oriented Architecture)框架是一种基于领域驱动设计(DDD)和面向对象设计原则的架构风格,旨在帮助开发人员构建清晰、可维护和可扩展的应用程序。

1)适配层(AdapterLayer):负责对前端展示(web,wireless,wap)的路由和适配,对于传统B/S系统而言,adapter就相当于MVC中的controller;

2)应用层(ApplicationLayer):主要负责获取输入,组装上下文,参数校验,调用领域层做业务处理,如果需要的话,发送消息通知等。层次是开放的,应用层也可以绕过领域层,直接访问基础实施层;

3)领域层(Domain Layer):主要是封装了核心业务逻辑,并通过领域服务(Domain Service)和领域对象(DomainEntity)的方法对App层提供业务实体和业务逻辑计算。领域是应用的核心,不依赖任何其他层次;

4)基础实施层(InfrastructureLayer):主要负责技术细节问题的处理,比如数据库的CRUD、搜索引擎、文件系统、分布式服务的RPC等。此外,领域防腐的重任也落在这里,外部依赖需要通过gateway的转义处理或者代理处理,才能被上面的App层和Domain层使用。

gateway 代理的架构,也非常类似与适配器的架构。gateway 代理架构分为两个部分:

  • gateway 接口处于 领域层,

  • gateway 的实现处于 基础实施层(InfrastructureLayer)。

具体如下:

图片

在COLA框架中,Gateway通常指的领域层的网关,是一种隔离在应用程序领域层内部和系统外部之间进行通信和交互的接口或者组件。COLA框架中的Gateway通常被用于以下几个方面:

  1. 数据交互:Gateway可以用于在领域层和基础设施层之间进行数据交互。例如,领域层的服务可以通过Gateway将数据传递给基础设施层进行持久化,或者基础设施层可以通过Gateway将数据加载到领域层中进行处理。

  2. 外部接口调用:Gateway也可以用于与外部系统进行通信。例如,Gateway可以封装与外部服务的交互逻辑,使得领域层不直接依赖于外部服务的细节。

  3. 跨边界通信:在复杂的应用程序中,可能存在多个边界或子系统。Gateway可以用于在这些边界之间进行通信,帮助将系统拆分为更小的模块并保持模块之间的松耦合性。

总之,COLA框架中的Gateway扮演着连接不同层和组件之间的桥梁角色,有助于实现应用程序的模块化、可维护和可扩展性。

问题2:什么是聚合?

“聚合”即“高内聚,低耦合”中的“内聚”之意;

聚合是业务和逻辑紧密关联的实体和值对象组合而成,聚合是数据修改和持久化的基本单元,一个聚合对应一个数据的持久化;聚合在DDD分层架构中属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑,聚合内的实体以充血模型实现个体业务能力,以及业务逻辑的高内聚;跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现;

首先我们来看下聚合模式的定义:

将实体和值对象划分为聚合并围绕着聚合定义边界。选择一个实体作为每个聚合的根,并仅允许外部对象持有对聚合根的引用。作为一个整体来定义聚合的属性和不变量,并把其执行责任赋予聚合根或指定的框架机制。

一个聚合包含一个聚合根(aggregation root)和一些相关的其他领域对象。

问题3:什么是聚合根?

聚合根中的“聚合”即“高内聚,低耦合”中的“内聚”之意;而“根”则是“根部”的意思,也即聚合根是一种统领式的存在。

事实上,并不存在一个教科书式的对聚合根的理论定义,你可以将聚合根理解为一个系统中最重要最显著的那些名词,这些名词是其所在的软件系统之所以存在的原因。

为了给你一个直观的理解,以下是几个聚合根的例子:

  • 在一个电商系统中,一个订单(Order)对象表示一个聚合根

  • 在一个CRM系统中,一个客户(Customer)对象表示一个聚合根

  • 在一个银行系统中,一次交易(Transaction)对象表示一个聚合根

软件中的概念已经很多了,为什么还要搞出个聚合根的概念?这里至少有2点原因:

  1. 聚合根遵循了软件中“高内聚,低耦合”的基本原则

  2. 聚合根体现了一种模块化的原则,模块化思想是被各个行业所证明的可以降低系统复杂度的一种思想。所谓的DDD是“软件核心复杂性应对之道”,也即这个意思,它将软件系统在人脑中所呈现地更加有序和简单,让人可以更好地理解和管控软件系统。

在实际项目中识别聚合根时,我们需要对业务有深入的了解,因为只有这样你才知道到底哪些业务逻辑是内聚在一起的。事实上,如果让一个从来没有接触过DDD的人来建模,十有八九也能设计出上面的订单、客户和交易对象出来。DDD绝非什么颠覆式的发明,只是在前人基础上的一种进步而已,这种进步更多的体现在一些设计原则上。

DDD聚合和 MYSQL 表的对应关系是什么 ?

Domain 聚合属于领域建模。MySQL 数据表设计属于 DB 建模。

注意,从理论上来说,DB建模(数据表建模)和领域建模没有关系,二者在处于不同的 设计阶段:

  • 领域建模处于 概要设计阶段,理清楚业务的内外边界,业务的内外流程,成果物为 HLD 概要设计文档。

  • DB建模 处于 详细设计阶段, 理清楚业务的“关系数据模型”进而推导出“物理数据模型”,成果物为 LLD 详细设计文档。

具体如下图

图片

数据建模的经典方法就是 E-R Model建模, 而当我们做MYSQL E-R建模的时候,我们需要考虑:

  • 需要建什么主表 - 可以类比为对象(当然也有些表只是为了关系映射)

  • 表里面需要什么列 - 可以类比为这对象所需的属性

  • 需要什么关联 - 表现数据对象与数据对象之间的联系

当然,E-R Model是数据模型的一种表现形式(数据建模不只是E-R Model一种表现形式) ,E-R Model以数据为中心,关注的是对象的实体和关系,建模时并不考虑Entity的行为。

在E-R概念模型的基础上可以建立“关系数据模型”进而推导出“物理数据模型”,这是一条以E-R Model为起始的数据建模的路线

ER模型分为实体、属性、关系三个核心部分。实体是长方形体现,而属性则是椭圆形,关系为菱形。 ER模型的实体(entity)即数据模型中的数据对象,例如人、学生、音乐都可以作为一个数据对象,用长方体来表示,每个实体都有自己的实体成员(entity member)或者说实体对象(entity instance),例如学生实体里包括张三、李四等,实体成员(entity member)/实体实例(entity instance) 不需要出现在ER图中。

ER模型的属性(attribute)即数据对象所具有的属性,例如学生具有姓名、学号、年级等属性,用椭圆形表示,属性分为唯一属性( unique attribute)和非唯一属性,唯一属性指的是唯一可用来标识该实体实例或者成员的属性,用下划线表示,一般来讲实体都至少有一个唯一属性。

ER模型的关系(relationship)用来表现数据对象与数据对象之间的联系,例如学生的实体和成绩表的实体之间有一定的联系,每个学生都有自己的成绩表,这就是一种关系,关系用菱形来表示。 ER模型中关联关系有三种:

1对1(1:1):1对1关系是指对于实体集A与实体集B,A中的每一个实体至多与B中一个实体有关系;反之,在实体集B中的每个实体至多与实体集A中一个实体有关系。1对多(1:N):1对多关系是指实体集A与实体集B中至少有N(N>0)个实体有关系;并且实体集B中每一个实体至多与实体集A中一个实体有关系。多对多(M:N):多对多关系是指实体集A中的每一个实体与实体集B中至少有M(M>0)个实体有关系,并且实体集B中的每一个实体与实体集A中的至少N(N>0)个实体有关系。

在数据建模中通常将E-R Model就称之为概念数据模型,接下来的层面是关系数据模型和物理数据模型;

而Domain 建模是属于对象建模、业务建模的范畴,Domain 聚合和MySQL 数据表 二的区别是职责不同, ER建模关心数据如何存储,Domain 对象建模需要为对象建立职责(对象的行为)。

当然Domain 建模 和E-R Model的分析具有很大的相似性,在一些极为简单的业务系统、业务场景中,二者可以做简单的映射:

  • Domain 模型里边 聚合根 可以映射到 E-R Model的 主表

  • Domain 模型里边 普通实体、值对象 可以映射到 E-R Model的 关联表

图片

问题:什么是领域驱动设计(DDD)?

什么是DDD

领域驱动设计最初由Eric Evans提出,2004年著名建模专家eric evans(埃里克埃文斯)发表的他最具影响力的书籍:

《domain-driven design –tackling complexity in the heart of software》(中文译名:领域驱动设计—软件核心复杂性应对之道)一书。标志着 DDD 这种 设计和架构方法的诞生。

我们在日常开发中,经常针对一些功能点争论“这个功能不应该我改,应该是你那边改”,最终被妥协改了之后都改不明白为什么这个功能要在自己这边改。

区别于传统的数据驱动架构(Data Driven Design)设计,领域驱动设计(DDD)也许在这个时候能帮助你做到清晰的划分。但是多年以来一直停留在理念阶段,然后,真正能实现并且落地的项目和公司少之又少,近来,包括阿里在内很多大厂,都在大力推行DDD的设计方法,它主要可以帮助我们解决传统单体式、集中式、大泥球架构难以快速响应业务需求落地的问题,并且针对中台和微服务盛行的场景做出指导。

图片

DDD为我们提供的是架构设计的方法论,既面向技术也面向业务,从业务的角度,自顶向下来把握设计方案。

图片

DDD的巨大价值

图片

统一思想:统一项目各方业务、产品、开发对问题的认知,明确组织当中产品、业务、架构、开发的角色和如何配合。通过统一的语言、明确的定义,统一方方面面的理解误差和理解分歧。

DDD工具链能帮助进一步强化能力逻辑的表达,借助DDD可视化流程构建专业知识库,能快速提升产品和技术、技术和技术之间的沟通效率,帮助开发同学快速、直观的了解业务,进而提高技术同学对业务有快速/全局了解,快速+全局的业务了解,反向帮助开发通讯能够方便把握细节,降低反复、返工、甚至推导重来的概率。

动态建模:需求是不断变化的,传统HLD、LLD建模都是核心思想是静态建模,缺乏有效的动态建模方法论和工具。DDD通过从领域事件、领域命令出发对领域对象进行建模,可以真实的反映这些变化。更好的通过边界划分将复杂业务领域简单化,将隐藏的业务显性化, 隐式流程显性化, 隐式字段显性化,帮助我们设计出清晰的领域边界,准确的业务流程,可以很容易地实现业务和技术统一的架构演进;

拉通“断层”:传统的需求常常是一句话需求,模型设计常常是工程师负责, 工程师的设计路径是 库表驱动+界面驱动,结合常规MVC三层架构进行自底向上的设计,完美的实现了业务人员和编码人员断层,需求和开发隔离的目标。DDD拉通了业务和编码,对常规MVC开发模式做一个反转,以业务为主导,自顶向下的进行领域模型设计,拉通业务和编码之间的巨大“断层”,使得代码更能反馈业务,反哺业务,提升代码逻辑的准确度和生命值。

业务人员和编码人员隔离的原因:轻设计、重编码。一句话需求,敏捷迭代、极速迭代、轻量级的流程。很容易把代码弄得杂乱无章,混乱不堪。

彻底“反腐”:首先是领域模型与数据模型分离,用领域模型来界定哪些需求在什么地方实现,保持结构清晰,隔离数据模型、存储模型的变化和腐败。除了数据模型,咱们应用 中有许多容易"腐败"的操作,比如直接的外部依赖,例如 MyBatis 的 Mapper 类、HttpClient 注入、RocketMQ 的监听、缓存的直接操作等,DDD通过防腐层的架构设计,实现 业务代码的彻底反腐败。

简单来说:

  • “反腐败”设计,从一半业务 + 一半技术,提升到 业务代码 和 基础设施解耦。

  • “反腐败”设计,使得领域代码更有业务纯度。

同时,DDD帮助沉淀各领域的业务,标准化流程链路,各领域间无耦合,沉淀的领域能力能够很好的复用,粗粒度的应用能力能基于细粒度领域能力去构建,构建好的能力可以在其他场景直接复用,提高开发效率,最终提升 领域代码的生命力、复用力。

提升“测维扩”能力:

可维护性 = 当依赖变化时,有多少代码需要随之改变。传统MVC三层架构,面临各种库升级、依赖服务升级、中间件升级、jar包冲突、微服务框架、存储扩容升级等依赖变化工作,需要从上到下,每一层的代码都要动,可维护性差。

可扩展性 = 做新需求或改逻辑时,需要新增/修改多少代码。在库表驱动开发、库表驱动架构的开发流程中,一般做第一个需求都非常的快,但是由于代码复用性低,越做到后面,做第N个需求时需要的时间很有可能是呈指数级上升的,绝大部分时间花费在老功能的重构和兼容上,最终你的创新速度会跌为0,促使老应用被推翻重构。

可测试性 = 运行每个测试用例所花费的时间 * 每个需求所需要增加的测试用例数量。在库表驱动开发、库表驱动架构的开发流程中,由于设施搭建困难、用例笨重运行耗时长、业务耦合度高用例爆炸等原因,业务代码很难有比较好的测试覆盖,而绝大部分的业务代码上线前的测试属于人肉的“集成测试”。低测试率导致我们对代码质量很难有把控,容易错过边界条件,异常case只有线上爆发了才被动发现。

最终这个应用变成了一个不敢升级、不敢部署、不敢写新功能、并且随时会爆发的炸弹,终有一天会给你带来惊喜。

DDD根本上解决上面的问题,提升“测维扩”能力。

降本增效: 以爱奇艺DDD落地案例为例,其会员业务部门在打赏业务中实践 DDD 后,取得了以下显著成果:新需求接入开发成本节约20%;更换底层中间件开发成本节约20%;项目熟悉成本节约30%(对DDD有基本了解为前提);单测开发成本指数级降低;上线风险、成本降低

DDD宏观概念

1. 领域、子域

领域可以理解为所要面和的客户要开发的软件的业务范围和业务逻辑。由于领域很大,所以只能关注领域的的某个方面,并进行相关的软件建模,这个软件模型又称之为“领域模型”。领域又可按照业务归类逻辑拆分为相互分离的子领域。

为什么要相互分离?

理解一下面向接口的编程就可以了,子域与子域间通过接口关联通信,隐藏子域内部细节。

2. 限界上下文

限界上下文可以理解为:

  1. 一般情况,是一个子域对应一个限界上下文,也就是说,我们拆分领域为分离的子域,也就是在拆限界上下文

  2. 在限界上下文内,各种术语、流程、代号有固定的意思。而这些术语、流程、代号在别的限界上下文就可能不是一样的意思。所以才会使用限界上下文。

    举个例子:在软件开发范畴内(限界上下文),质量是代码经过单元测试和集成测试、自动化测试、安全测试等一系统测试的软件,如果在建筑工程范畴,这个质量就是另一种意思了。

    很简单的例子,“顾客”在订单子域是下单付费的登录用户,而在产品目录子域,“顾客”则是所有浏览商品的用户,包括匿名未登录的和已登录的用户,同一个术语在不同的子域里有不同的意思,所以这两个子域不应规划到一个限界上下文里。

  3. 甚至一个子域可能包括多个限界上下文,理论上讲应该将这个子域拆分为更细的子域,以达到一个子域对应一个限界上下文的标准。

  4. 可以粗粗地认为限界上下文就是子域关联的一系列的术语、流程和代号。确定统一语言的上下文

  5. 当关注到一个子域时,该子域即是核心域,而与其集成的支撑域和通用子域,为了支持该核心域业务方面,该核心域的限界上下文会包含支撑域、通用子域的一部分或全部。

限界上下文包含哪些领域模型呢?

  1. 实体

  2. 值对象

  3. 聚合

  4. 领域事件

  5. 领域服务

  6. ....

限界上下文是一个显式的边界,领域模型便存在于这个边界之内。可以认为是namespace。限界上下文并不只是局限于容纳模型,它通常标定了一个系统、一个应用程序或者一种业务服务。

3. 核心域、支撑域、通用子域
  1. 核心域,可以理解为领域中你关注的问题空间的业务子域。比如说,电商系统领域,订单子域是关于订单业务的,所以是核心域。

  2. 支撑域,可以理解为领域中涉及业务但是不是核心业务的子域。比如说,商品品类子域,它是关于业务的,但是相对于核心域来说,它不是核心业务,但是它给核心域提供了商品查询支持,所以它是支撑域。

  3. 通用子域,被所有子域需要的子域,一般只提供数据、接口,但不是涉及业务相关。比如说帐号子域,它并不涉及电商的业务,但是却给核心子域提供了帐号相关的数据资料。它属于工具类型,可以理解为对核心域提供接口支持的都可以称之为通用子域。

核心域是相对的。当你要解决什么问题,那么该问题对应的子域就是核心域,所以对该子域提供支持的都是支撑域和通用子域。

图片

DDD微观概念

领域驱动设计围绕着领域模型进行设计,通过分层架构将领域独立出来。

这里有两个关键词:

  • 领域模型

  • 分层架构

领域模型的对象包括:实体、值对象和领域服务,领域逻辑都应该封装在这些对象中。

领域模型 Domain Model

领域反映到代码里就是模型,模型是对领域某个方面的抽象,并且可以用来解决相关域的问题,Domain Model 的基础单元,分为实体和值对象两种。实体和值对象,二者是领域模型中非常重要的基础领域对象(Domain Object,DO)。

图片

实体对象 Entities

有唯一标志的核心领域对象,且这个标志在整个软件生命周期中都不会发生变化。这个概念和我们平时软件模型中和数据库打交道的Entity实体比较接近,不同的是DDD中这些实体会包含与该实体相关的业务逻辑,它是操作行为的载体。

白话解释:实体就是对象的方法和属性实现业务逻辑的类,一般由唯一标识id和值对象组成,属性发生改变,可以影响类的状态和逻辑。

实体 = 唯一身份标识 + 可变性【状态 + 行为】

DDD 中要求实体是唯一的且可持续变化的。意思是说在实体的生命周期内,无论其如何变化,其仍旧是同一个实体。唯一性由唯一的身份标识来决定的。可变性也正反映了实体本身的状态和行为。

实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。

比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。

值对象 Value Object

依附于实体存在,通过对象属性来识别的对象,它将一些相关的实体属性打包在一起处理,形成一个新的对象。

这些对象是用来表示临时的事物,或者可以认为值对象是实体的属性,这些属性没有特性标识但同时表达了领域中某类含义的概念。

通常值对象不具有唯一id,由对象的属性描述,可以用来传递参数或对实体进行补充描述。

白话解释:不关心唯一性,具有校验逻辑、等值判断逻辑,只关心值的类。

比如用户实体,包含用户名、密码、年龄、地址,地址又包含省市区等属性,而将省市区这些属性打包成一个属性集合就是值对象。

值对象与实体的区别是什么?

  • 值对象没有唯一标识和连续性,任何属性发生变化, 都可以认为是新的值对象。判断对象是否相同:值对象需要判断所有属性是否相同,而实体只需要判断唯一标识是否相同。

  • 值对象一般依附于实体而存在,是实体属性的一部分,而非独立存在。值对象属性是只读的,可以被安全的共享。

值对象 = 将一个值用对象的方式进行表述,来表达一个具体的固定不变的概念。

还是举个订单的例子,订单是一个实体,里面包含地址,这个地址可以只通过属性嵌入的方式形成的订单实体对象,也可以将地址通过 json 序列化一个 string 类型的数据,存到 DB 的一个字段中,那么这个 Json 串就是一个值对象,是不是很好理解?

当你只关心某个对象的属性时,该对象便可作为一个值对象。

我们需要将值对象看成不变对象,不要给它任何身份标识,注意,不要给它任何身份标识,该对象便可作为一个值对象。

注意:应该尽量避免像实体对象一样的复杂性。

聚合(aggregate)

实体和值对象表现的是个体的能力,而我们的业务逻辑往往很复杂,依赖个体是无法完成的,这时候就需要多个实体和值对象一起协同工作,而这个协同的组织就是聚合。

聚合是数据修改和持久化的基本单元,同一个聚合内要保证事务的一致性,所以在设计的时候要保证聚合的设计拆分到最小化以保证效率和性能。

白话解释:就是对象之间的关联,只是规定了关联对象规则,操作聚合时,类似于操作Hibernate中的One-Many对象的概念。

在DDD中,实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有相对丰富的业务属性和业务行为。而值对象主要是属性集合,主要完成对实体的状态和特征描述。

聚合在 DDD分层架构里属于领域层,同一个微服务的领域层可以有多个聚合,每个聚合内有一个聚合根,多个实体、值对象和领域服务等领域对象。同一个限界上下文内的多个聚合,通过应用层组合在一起共同实现了领域模型的核心领域逻辑。

我们为每一个聚合设计一个仓储完成聚合数据的持久化操作。为了避免聚合数据频繁地提交,建议你尽可能将聚合内变更的数据,封装在一次交易中提交仓储完成持久化。

聚合在领域模型里是一个逻辑边界,它本身没有业务逻辑实现相关的代码。聚合的业务逻辑是由聚合内的聚合根、实体、值对象和领域服务等来实现的。聚合内的实体以充血模型实现自身的业务逻辑。跨多个实体的领域逻辑通过领域服务来实现。比如,有的业务场景需要同-个聚合的A和B两个实体来共同完成,我们就可以将这段业务逻辑用领域服务组合A和B两个实体来完成。

跨多个聚合的业务逻辑的组合和编排,是通过应用服务来实现的。比如,有的业务逻辑需要聚合C和聚合D中的两个领域服务来共同完成,为了避免聚合之间的领域服务直接调用,实现微服务内聚合解耦,此时你可以将这段业务逻辑上升到应用层,通过应用服务组合两个聚合的领域服务来实现。

聚合是领域对象的显式分组,我们把一些关联性极强、生命周期一致的实体、值对象放到一个聚合里。聚合定义了一组具有内聚关系的相关对象的集合,每个聚合都有一个根对象(聚合根实体)。

我们把聚合看作是一个修改数据的单元。一个聚合是一组相关的被视为整体的对象。每个聚合都有一个根对象(聚合根实体),从外部访问只能通过这个对象。

根实体对象有组成聚合所有对象的引用,但是外部对象只能引用根对象实体。只有聚合根才能使用仓储库直接查询,其它的只能通过相关的聚合访问。如果根实体被删除,聚合内部的其它对象也将被删除。

为什么需要进行聚合?

旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。聚合有两个核心要素:

  • 一个聚合根

  • 一个上下文边界

这个边界 根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的服务很自然就是“高内聚、低耦合”的。聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。

聚合根(aggregate root)

如果把聚合比作组织,那聚合根就是这个组织的负责人。也叫做根实体,一个特殊的实体,它是聚合的管理者,代表聚合的入口,抓住聚合根可以抓住整个聚合。

聚合根也称为根实体,它不仅是实体,还是聚合的管理者。

  • 首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。

  • 其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。

  • 最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

聚合是一种边界,它可以封装一到多个实体与值对象,并维持该边界范围之内的业务完整性。在聚合中,至少包含一个实体,且只有实体才能作为聚合根。

简单概括一下:

  • 通过事件风暴(简单理解就是头脑风暴,一般是先通过个人理解,然后再和相关核心人员进行沟通),得到实体和值对象;

  • 将这些实体和值对象聚合为“投保聚合”和“客户聚合”,其中“投保单”和“客户”是两者的聚合根;

  • 找出与聚合根“投保单”和“客户”关联的所有紧密依赖的实体和值对象;

  • 在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。

服务(services)

服务提供的操作是它提供给使用它的客户端,并突出领域对象的关系。

所有的service只负责协调并委派业务逻辑给领域对象进行处理,其本身并未真正实现业务逻辑,绝大部分的业务逻辑都由领域对象承载和实现了。service可与多种组件进行交互,这些组件包括:其他的service、领域对象和repository 或 dao。

服务又细分为领域服务和应用服务。

领域服务(Domain Service)

领域中的一些概念,如果是名词,适合建模为对象的一般归类到实体对象或值对象。如果是动词,比如一些操作、一些动作,代表的是一种行为,如果是和实体或值对象密切相关的,也可以合并到某个实体或者值对象中。

但是,有些操作不属于实体或者值对象本身,或会涉及到多个领域对象,并且需要协调这些领域对象共同完成这个操作或动作,这时就需要创建领域服务来提供这些操作。

有些领域的操作是一些动词,并不能简单的把他们归类到某个实体或者值对象中。领域的动作,从领域中识别出来之后,应该将它声明成一个服务,它的作用仅仅是为领域提供相应的功能。

简单理解:就是跨多个领域对象的业务方法。

当一些逻辑不属于某个实体时,可以把这些逻辑单独拿出来放到领域服务中。可以使用领域服务的情况:

  • 执行一个显著的业务操作

  • 对领域对象进行转换

  • 以多个领域对象作为输入参数进行计算,结果产生一个值对象

领域服务有两个特征:

  • 操作代表了一个领域概念,且不是实体或者值对象的一个自然的部分;

  • 被执行的操作涉及领域中的其他对象;操作是无状态的。领域服务还有一个好处可以避免领域逻辑泄露到应用层。

因为如果没有领域服务,那么应用层会直接调用领域对象完成本该是属于领域服务该做的操作。此外,如果实体操作过多或者过大,为了避免臃肿,也可以使用领域服务来解决。但是,不能把所有的东西都搬到领域服务里,过度使用可能会导致产生的太多的贫血对象。

理想的情况是没有领域服务,如果领域服务使用不恰当,慢慢又演化回了以前逻辑都在 service 层的局面。

应用服务(Application Service)

应用层作为展现层与领域层的桥梁,是用来表达用例和用户故事的主要手段。应用层通过应用服务接口来暴露系统的全部功能。

在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。

应用层相对来说是较“薄”的一层,除了定义应用服务之外,在该层我们可以进行安全认证,权限校验,持久化事务控制,或者向其他系统发生基于事件的消息通知,另外还可以用于创建邮件以发送给客户等。

领域服务和应用服务的不同:

  • 领域服务和应用服务是不同的,领域服务是领域模型的一部分,用来处理业务逻辑,而应用服务不是。

  • 应用服务是领域服务的直接客户,负责处理事务、安全等操作,它将领域模型变成对外界可用的软件系统。

  • 跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。

跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。

  • 比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现;

  • 而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务。

领域事件/领域命令

领域事件是一个领域模型中极其重要的部分,用来表示领域中发生的事件。

图片

领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。

程序事件通常分为:系统事件、应用事件和领域事件。领域事件的触发点在领域模型中。

它的作用是将领域对象从对repository或service的依赖中解脱出来,避免让领域对象对这些设施产生直接依赖。它的做法就是当领域对象的业务方法需要依赖到这些对象时,就发出一个事件,这个事件会被相应的对象监听到并做出处理。譬如跨限界上下文时,使用关键应用事件触发事件传递。

领域事件是对 repository或service 的异步解耦。例如,在协程中,就有 EventBus 这样的事件总线,完成模块之间的异步解耦,也有 RocketMQ 这样的分布式消息组件,完成进程级别的异步解耦。

而领域事件是设计维度的解耦。EventBus 、RocketMQ是实现层面的异步解耦,当然是先有设计,后有实现。

在DDD中,通过领域事件,一个领域模型可以忽略不相关的领域活动,同时明确领域专家要跟踪或希望被通知的事情,或与其他模型对象中的状态更改相关联。

下面简单说明领域事件:

  • 事件发布:构建一个事件,需要唯一标识,然后发布;

  • 事件存储:发布事件前需要存储,因为接收后的事建也会存储,可用于重试或对账等;

  • 事件分发:服务内直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等;

  • 事件处理:先将事件存储,然后再处理。

比如下订单后,给用户增长积分与赠送优惠券的需求。如果使用瀑布流的方式写代码。一个个逻辑调用,那么不同用户,赠送的东西不同,逻辑就会变得又臭又长。这里的比较好的方式是,用户下订单成功后,发布领域事件,积分聚合与优惠券聚合监听订单发布的领域事件进行处理。

在特定的领域由用户动作触发,表示发生在过去的事件,或者领域状态的变化。比如:

  • 充值成功

  • 充值失败的事件。

资源仓储/资源库(Repository)

仓储(资源库)是用来管理实体的集合。仓储介于领域模型和数据模型之间,主要用于聚合的持久化和检索。

仓储里面存放的对象一定是聚合,原因是domain是以聚合的概念来划分边界的;聚合作为一个整体概念,要么一起被取出来,要么一起被删除。它隔离了领域模型和数据模型,以便我们关注于领域模型而不需要考虑如何进行持久化。

我们将暂时不使用的领域对象从内存中持久化存储到磁盘中。

当日后需要再次使用这个领域对象时,根据 key 值到数据库查找到这条记录,然后将其恢复成领域对象,应用程序就可以继续使用它了,这就是领域对象持久化存储的设计思想。

工厂(Factory)

工厂用来封装创建一个复杂对象尤其是聚合时所需的知识,作用是将创建对象的细节隐藏起来。客户传递给工厂一些简单的参数,然后工厂可以在内部创建出一个复杂的领域对象然后返回给客户。

工厂(Factory)不是必须的,只有当创建实体和值对象复杂时,建议使用工厂模式。工厂和资源库都是对领域对象生命周期的管理。工厂负责领域对象的创建,用于封装复杂或者可能变化的创建逻辑。资源库负责从存放资源的持久层获取、添加、删除或者修改领域对象。  

通过大量的总结和梳理,归纳总结出来了一个核心的DDD建模理论:

  • DDD建模的两个阶段

  • DDD建模的四个标准动作

具体如下图

图片

接下来,看看DDD四种模型

图片

失血模型

领域模型中只有简单的get set方法,是对一个实体最简单的封装,其他所有的业务行为由服务类来完成。pojo里边光秃秃的,get和set方法都没有。

@Data
@ToString
public class User {
private Long id;
private String username;
private String password;
private Integer status;
private Date createdAt;
private Date updatedAt;
private Integer isDeleted;
}

而且,其他所有的业务行为由服务类来完成,

public class UserService{
public boolean isActive(User user){
return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
}
}

贫血模型

贫血模型是指领域对象里只有get和set方法(POJO),所有的业务逻辑(不包含对象的状态变化在内),放在Business Logic层。

Domain Object(领域对象)模型包含对象属性的定义和操作对象属性的getter/setter方法并包含了对象的行为(例如:就像一个完整的人,具有一些属性如姓名、性别、年龄等,还具有一些能力,如走路、吃饭、恋爱等,这样才是一个完整的对象), 但不包含依赖Dao层(持久层)的业务逻辑。

这部分依赖于Dao层的业务逻辑将会放到Business Logic层(业务逻辑层)中的服务类来实现,组合逻辑也由服务类负责。可以看出,贫血模型中的领域对象是不依赖于持久层的。

代码架构层次结构是:Client-> Business Facade Service -> Business Logic Service(Business Logic Service是依赖Domain Object的行为) -> Data Access Service

@Data
@ToString
public class User {
private Long id;
private String username;
private String password;
private Integer status;
private Date createdAt;
private Date updatedAt;
private Integer isDeleted;

public boolean isActive(User user){
return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
}

public void setUsername(String username){
return username.trim();
}
}

贫血模型在失血模型基础之上,领域对象的包含一些状态变化,但是停留在内存层面,不关心数据持久化。贫血模型所有的业务逻辑都不包含在内而是放在Business Logic层。

图片

贫血模型优点是系统的层次结构清楚,各层之间单向依赖,Client->(Business Facade)->Business Logic->Data Access Object。可见,领域对象几乎只作传输介质之用,不会影响到层次的划分。这就是 传统的 数据驱动的开发。

在使用Spring的时候,通常暗示着你使用了贫血模型,我们把Domain类用来单纯地存储数据,Spring管不着这些类的注入和管理,Spring关心的逻辑层(比如单例的被池化了的Business Logic层)可以被设计成singleton的bean。

假使我们这里逆天而行,硬要在Domain类中提供业务逻辑方法,那么我们在使用Spring构造这样的数据bean的时候就遇到许多麻烦,比如:bean之间的引用,可能引起大范围的bean之间的嵌套构造器的调用。

充血模型

在贫血模型基础上,负责数据的持久化。

@Data
@ToString
public class User {
private Long id;
private String username;
private String password;
private Integer status;
private Date createdAt;
private Date updatedAt;
private Integer isDeleted;

private UserRepository userRepository;

public boolean isActive(User user){
return user.getStatus().equals(StatusEnum.ACTIVE.getCode());
}

public void setUsername(String username){
this.username = username.trim();
userRepository.update(user);
}
}

充血模型层次结构和上面的差不多,不过大多业务逻辑放在Domain Object里面,应用服务层 Business Logic只是简单封装部分业务逻辑以及控制事务、权限等,

这样层次结构就变成Client->(Business Facade)->Business Logic->Domain Object->Data Access Object。

图片

它的优点是面向对象,Business Logic符合单一职责,不像在贫血模型里面那样包含所有的业务逻辑太过沉重。

胀血模型

service都不需要,所有的业务逻辑、数据存储都放到一个类中。对于DDD来说,失血和胀血都是不合适的,失血太轻量没有聚合,胀血那是初学者才这样写代码。

那么充血模型和贫血模型该怎么选择?

充血模型依赖repository接口,与数据存储紧密相关,有破坏程序稳定性的风险。

贫血模型与充血模型区别
区别充血模型(DDD开发方式下)贫血模型(传统OOP开发方式下)

编码方式

以领域对象为主的行为状态转换

事务脚本(CRUD、判断、循环、检查等逻辑只是简单的方法级别拆分)

Service

跨领域逻辑的组合

事务脚本的对外包装

Domain

一个领域的entity集合包

VO/DTO

Entity

包含了一个领域里的状态、以及对状态的直接操作单元,具有自闭环的对象。

VO/DTO

ValueObject

简单的基本值对象,如电话号码、地址等,组成entity的基本单元

语言基本类型

Aggregation

(有点类似数学中集合的定义)将实体和值对象划分为聚合并围绕着聚合定义边界。

多个VO/DTO

Aggregation Root

唯一可以给外界作为某个Aggregation引用的entity。

单个VO/DTO

优缺点
  • 贫血模型的优缺点

    • 优点1:很传统的编程思路,被许多程序员所掌握,许多教材采用的是这种模型,对于初学者,这种模型很自然,甚至被很多人认为是java中最正统的模型。

    • 优点2:思路清晰,事务边界清晰一般来说service的每个方法都可以看成一个事务,因为通常Service的每个方法对应着一个用例

    • 缺点:对象状态与行为分离,不能直观地描述领域对象。行为的设计主要考虑参数的输入和输出而非行为本身,不太具有面向对象设计的思考方式。

  • 充血模型的优缺点

    • 优点:贫血model偏重个性化,面向过程式。充血偏共性化,采用OO设计,类拥有其属性及对应的行为,通过将职责分配到相应的模型对象或Service,可以很好的组织业务逻辑,因此非常适合于复杂的企业业务逻辑的实现,以及可复用程度比较高。

    • 缺点1:领域驱动建模要求对领域模型完整而透彻的了解,只给出一个用例的实现步骤是无法得到领域模型的,这需要和领域专家的充分讨论。错误的领域模型对项目的危害非常之大,而实现一个好的领域模型非常困难。

    • 缺点2:对象高度自洽的结果是不利于大规模团队分工协作。一个编程个体至少要完成一个完整业务逻辑的功能。对于单个完整业务逻辑,无法再细分下去了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值