目录
前言
在日常工作中,接手或维护的工程,大多数使用的是三层架构,即controller、service、dao三层,在使用的过程中,会遇到很多问题:
-
面向数据建模,面向过程编程,没有真正“面向对象”
-
只注重结果,不注重过程,service层动辄数百上千行,充斥着过程代码、胶水代码,要么臃肿、要么流水账、要不重复、要么逻辑分散,后期极难维护
-
代码耦合严重,层与层之间互相调用、逆向调用,牵一发而动全身
-
代码无法体现业务,在大家都不爱写注释的情况下,随着时间的推移,代码业务逻辑将无人理解,不敢改也改不动。
那么有没有一个好的解决方案呢?今天我们讲讲领域驱动模型DDD。
由于领域模型与我们传统的贫血三成MVC模型差别较大,很多概念及思维方式均需要重新理解和刷新,整体内容非常庞大。本文先简单介绍下领域驱动模型涉及的一些基础概念和方法论。后续还会有几篇文章介绍DDD模型的具体落地实现。
一、什么是DDD
DDD(领域驱动设计)是一种处理高度复杂领域的设计思想,是一种架构设计方法论,是一种设计模式。以高内聚低耦合为目的,把一个复杂的软件应用系统中各个部分进行一个很好的拆解和封装,对软件系统进行模块化的一种思想。DDD不仅可以用于微服务设计,还可以很好地应用于企业中台的设计,也适用于传统的单体应用。
1.1. 通用语言
领域驱动设计,作为一个技术、产品、用户通用的语言进行沟通,极大地降低了沟通成本与沟通失真问题。
1.2. 为什么DDD如此重要
它直指软件开发的本质难题:业务与技术的断层。DDD既不是UI导向(SMART UI)、也不是数据导向(UML),而是以产品为导向,并不是用户所使用的产品,而是整个产品的业务逻辑。完美的解决了以上问题:
-
面向领域建模,面向对象编程,代码直接映射现实世界概念,贴近业务,离客户更近
-
领域逻辑高内聚,符合Java开发原则
-
技术细节变更如数据库、缓存、定时器等的变更对业务逻辑影响比较小,非常适合插件式架构
-
代码可读性、可维护性更强,对后续扩展、移植等支持更好,分层更加科学
适合中大型项目、API经济、中台化,可以借鉴DDD进行对微服务进行拆分。
1.3. DDD 的基础概念
DDD的核心思想是围绕业务领域进行设计,并通过精确的建模、明确的分层和团队协作来解决复杂性问题。其关键要素包括:
- 领域(Domain):
指的是软件所要解决的具体问题的领域。理解并定义清楚业务需求是DDD的基础。
- 领域模型(Domain Model):
领域模型是对领域的抽象和表达,通过一组类、对象、方法来描绘和实现业务领域的概念与规则。
- 限界上下文(Bounded Context):
指的是一个子系统的业务范围,域模型在每个限界上下文内都有特定的含义。在不同的上下文中,同一术语可能有不同的定义。
二、DDD的基本构成要素
2.1. 领域模型
领域模型是DDD的核心,通过实体(Entity)、值对象(Value Object)、聚合(Aggregate)、服务(Service)等元素来构建。每个元素有不同的职责和特性。
- 实体(Entity):
具有唯一标识的对象,通常可以跟踪生命周期,如客户、订单等。
- 值对象(Value Object):
没有唯一标识的对象,通常表示一些属性的组合,如地址、时间段等。
- 聚合(Aggregate):
多个相关实体和值对象的集合,是系统中具有一致性边界的一个单元,通常有一个聚合根来代表整个聚合。聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的服务很自然就是“高内聚、低耦合”的。
聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。
聚合有以下5个通用的设计原则:
(1)在一致性边界之内建模真正的不变条件
聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因。
(2) 设计小聚合
如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。
(3) 通过唯一标识引用其他聚合
聚合之间是通过关联外部聚合根ID的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。
(4)在边界之外使用最终一致性
聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦。
(5)通过应用层实现跨聚合的服务调用
为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。
从长远看来,遵循聚合原则对整个项目是有益的。我们将尽可能地保证一致性,并且致力于创建高性能的、高可伸缩性的系统。
-
聚合根
如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。最后在聚合之间,它还是聚合对外的接口人,以聚合根ID关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根ID关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。
聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。
- 服务(Service):
包含领域逻辑的功能性对象,适合放在领域模型之外的逻辑,不属于某个特定实体或值对象的功能。
-
核心域:
核心域就是一个领域的核心。比如说电商平台有哪些核心域:产品信息核心域、支付交易核心域、物流域,自然也就对应着产品信息上下文、支付上下文、物流上下文。
-
支撑子域:
支撑核心域正常工作的子域。比如用户行为分析域、风控域,自然也有对应的界限上下文。
-
通用子域:
一些业内通用的子域,比如用户认证系统,可以通过购买第三方的平台来实现。
2.2. 限界上下文(Bounded Context)
限界上下文是一个系统的业务边界,其中的术语、概念和模型是有具体含义的。不同的限界上下文可以有不同的模型和术语。例如,一个电商系统中,“订单”在订单服务中可能有一个模型,在支付服务中有另一个不同的模型。限界上下文之间可以通过上下文映射(Context Mapping)来进行集成。
2.3. 集成(Integration)
限界上下文之间可能需要通过接口、消息或事件等方式进行集成。常见的集成方式包括:
- 共享内核(Shared Kernel):
不同上下文共享一个核心的领域模型。
- 客户-供应商(Customer-Supplier):
一个上下文作为客户,另一个上下文提供服务,客户依赖供应商提供的接口。
- 防腐层(Anti-Corruption Layer):
通过为外部系统提供独立的接口,避免外部系统的变化影响到内部系统。
2.4. 领域事件(Domain Event)
领域事件用于表示系统中发生的一个具有业务意义的事件,它会通知其他部分系统某个业务过程的完成或变更。领域事件有助于促进松耦合的设计,并推动系统的异步处理。
三、DDD的优秀实践
3.1. 深入理解业务
DDD强调团队必须深入理解业务领域,与领域专家保持紧密的合作。这要求开发人员、产品经理、业务专家等共同参与到领域模型的构建过程中。
- 领域专家合作:
定期与业务专家沟通,确保开发团队理解真实的业务需求和挑战。
- 领域语言(Ubiquitous Language):
使用统一的领域语言,在所有参与者之间共享同一语言,避免出现歧义和误解。
DDD的领域划分
领域的拆分1
领域
3.2. 通过模型驱动设计
领域模型是DDD的核心,构建领域模型不仅仅是设计类和对象,还涉及如何通过模型表达业务逻辑。为此,DDD倡导以下做法:
- 模型演化:
随着对业务理解的不断深入,领域模型应当不断演化。
- 精确的领域划分:
通过限界上下文将复杂的系统拆分成多个小的、更容易管理的子系统。
3.3. 架构分层
DDD建议采用分层架构,通常包括以下几层:
ddd的工程架构层级说明
-
层级概述
-
层级功能说明
- 表示层(Presentation Layer):
处理与用户交互的部分。
- 应用层(Application Layer):
协调领域层和表示层之间的交互,处理用例逻辑。
- 领域层(Domain Layer):
包含领域模型和领域逻辑,是业务核心。
- 基础设施层(Infrastructure Layer):
提供数据库、网络等基础设施支持。
应用层和服务关系
3.4. 贫血模型
所谓的贫血模型是在定义对象时,指定以对象的属性信息,却没有对象的行为信息,最后再通过添加一些对象属性的get/set方法来赋值取值操作(实际上就是我们传统项目的Entity)。这些贫血对象在设计之初就被定义为只能包含数据,不能加入领域逻辑;所有的业务逻辑是放在所谓的业务层,需要使用这些模型来传递数据。
贫血领域模型的基本特征是:它第一眼看起来还真像这么回事儿。项目中有许多对象,它们的命名都是根据领域来的。对象之间有着丰富的连接方式,和真正的领域模型非常相似。但当你检视这些对象的行为时,会发现它们基本上没有任何行为,仅仅是一堆getter/setter。业务逻辑要全部写入一组叫Service的对象中;而Service则构建在领域模型之上,需要使用这些模型来传递数据。
这种反模式的恐怖之处在于:它完全和面向对象设计背道而驰。面向对象设计主张将数据和行为绑定在一起,而贫血领域模型则更像是一种面向过程设计。更糟糕的是,很多人认为这些贫血领域对象是真正的对象,从而彻底误解了面向对象设计的涵义。
3.5. 充血模型
面向对象设计的本质是:“一个对象是拥有状态和行为的”。比如一个人:他眼睛什么样鼻子什么样这就是状态;人可以去打游戏或是写程序,这就是行为。
举个简单的J2EE案例,设计一个与用户(User)相关功能。用户要打游戏,传统的设计是设计一个service层或Manager层,来实现打游戏的行为。为什么要有一个“用户Manager”这样的东西存在去帮人“打游戏”呢?
传统的设计一般是:
-
类:User+UserManager;
-
保存用户调用:userManager.save(User user)。
充血的设计则可能会是:
类:User User有一个行为是:保存它自己。
-
保存用户调用:user.save();
个人更倾向于总是使用充血模型,因为OOP总是比面向过程编程要有更丰富的语义、更合理的组织、更强的可维护性—当然也更难掌握。
因此实际工程场景中,是否使用,如何使用还依赖于设计者以及团队充血模型设计的理解和把握,因为现在绝大多数J2EE开发者都受贫血模型影响非常深。另外,实际工程场景中使用充血模型,还会碰到很多很多细节问题,其中最大的难关就是“如何设计充血模型”或者说“如何从复杂的业务中分离出恰到好处且包含语义的逻辑放到VO的行为中”。如果一个对象包含其他对象,那就将职责继续委托下去,由具体的 POJO 执行业务逻辑,将策略模式更加细粒度,而不是写 ifelse。
基于充血模型的 DDD 开发模式用类来描述业务模型(数据和功能),把原来又重又凌乱的service层逻辑拆分并转移至各领域(Domain)类内,对不同业务功能的数据和方法进行封装,提升了代码内聚性和复用性,也提高了代码可读性。由于类等同于业务模型,在功能不断迭代后就不会像贫血模型的service一样变得杂乱、模糊、难以维护,整个系统的代码看起来层次清晰,阅读逻辑简单易懂,也方便新人快速上手了解系统逻辑。无论是开发新功能还是老功能的迭代,我们优先考虑的是领域类如何定义、修改,因此称之为领域驱动设计。
3.6. 战略设计
DDD强调战略设计,即如何合理划分系统和模块,确保各模块之间的松耦合。战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。战略设计会建立领域模型,领域模型可以用于指导微服务的设计和拆分。包括以下内容:
- 划分限界上下文:
根据业务需求和领域逻辑进行合理的模块划分。
- 集成策略:
定义不同上下文之间的集成方式,如共享内核、事件驱动等。
- 反腐层(Anti-Corruption Layer):
防止外部系统的影响,保持内部系统的独立性。
3.7. 战术设计
战术设计犹如使用一把精小的画笔在领域模型上描绘着每个细枝末节。其中一个比较重要的工具被用来将若干实体和值对象以恰当的大小聚集在一起。这就是聚合(Aggregate)模式。
战术设计则从技术视角出发,侧重于领域模型的技术实现完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。
DDD就是以最明确而又可行的方式对领域进行建模。使用领域事件(Domain Events)既可以让你明确地建立模型,也可把模型内部发生的事情分享给需要知道这一切的系统
3.8. 领域服务
领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务了。可以使用领域服务的情况:
-
执行一个显著的业务操作
-
对领域对象进行转换
-
以多个领域对象作为输入参数进行计算,结果产生一个值对象
3.9. 应用服务
应用层作为展现层与领域层的桥梁,是用来表达用例和用户故事的主要手段。应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。
应用层相对来说是较“薄”的一层,除了定义应用服务之外,在该层我们可以进行安全认证,权限校验,持久化事务控制,或者向其他系统发生基于事件的消息通知,另外还可以用于创建邮件以发送给客户等。
3.10. 领域事件
将领域中所发生的活动建模成一系列的离散事件。每个事件都用领域对象来表示,领域事件是领域模型的组成部分,表示领域中所发生的事情。
领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。
领域事件是一个领域模型中极其重要的部分,用来表示领域中发生的事件。忽略不相关的领域活动,同时明确领域专家要跟踪或希望被通知的事情,或与其他模型对象中的状态更改相关联,领域事件包括以下几种:
-
事件发布:构建一个事件,需要唯一标识,然后发布;
-
事件存储:发布事件前需要存储,因为接收后的事件也会存储,可用于重试或对账等;
-
事件分发:服务内直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等;
-
事件处理:先将事件存储,然后再处理。
在做用户旅程或者场景分析时,我们要捕捉业务、需求人员或领域专家口中的关键词:“如果发生……,则……”“当做完……的时候,请通知……”“发生……时,则……”等。在这些场景中,如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件。
领域事件驱动设计可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。在领域模型映射到微服务系统架构时,领域事件可以解耦微服务,微服务之间的数据不必要求强一致性,而是基于事件的最终一致性。
领域事件的执行需要一系列的组件和技术来支撑;领域事件处理包括:事件构建和发布、事件数据持久化、事件总线、消息中间件、事件接收和处理等,如下图所示: