一文了解DDD(领域驱动设计)

224 阅读13分钟

领域

要了解领域驱动设计,首先需要搞清楚什么是领域。 所谓领域,本质上是对问题的一种统称,是一种业务展开的方式,领域解释了该业务的范围。可以是一个整体模块,也可以是整体模块下属的一部分,例如电商系统中有商品,订单等概念,其中电商系统可以称为领域,电商系统的商品,订单概念也可以称为领域。领域是问题的边界,确定了要解决的业务问题领域。

业务模型

业务模型是领域问题的解决方案,领域问题是对一个场景的归纳,框定了要解决的问题的上下文。

DDD和业务模型的联系

日常开发中,我们主要围绕着业务模型建模,建模手段各有不同,例如最典型也是最常用的数据建模。从数据的角度来规划对象的组织形式,并通过面向数据库的方式对这些数据对象进行设计和建模,这种开发模式通常被称为数据驱动模式。同样DDD也是业务模型建模的一个重要手段。

模型的维度

一个完整的业务模型应当包含至少以下7个维度。

1.业务描述

业务模型需要通过通用语言进行描述,使得所有参与人都能对模型代表的业务场景达成一致。白话来讲,就是对需求认知统一,避免由于某些用词不当造成大家理解有误差。

2.业务拆分

一般来说业务场景是很复杂的,涉及到的功能组件数量很多,这时候对业务的拆分可以明确各个组件功能的边界。

3.业务对象

在一个业务场景中必然存在一组相关的业务对象,这些对象通过一定的交互关系构建具体的业务场景。

4.业务规则

一个业务模型中,内部的核心逻辑通过一系列的业务规则来展现。

5.业务状态

每个场景都具有业务状态,这些状态构成了业务处理的流程和顺序。

6.业务数据

业务模型都会产生业务数据,业务规则和业务状态都是围绕数据处理过程展开的,其中核心的业务数据更需要持久化的存储。

7.业务外观

业务需要和客户端进行交互,需要一定的交互入口,这就是业务的外观。

image.png

领域驱动设计对以上维度的实现

领域驱动设计对以上7个维度给出了完整的设计方式

在DDD中对业务模型的思考被划分为两个维度,一是战略维度另一个是战术维度。

战略设计维度关注如何设计领域模型以及如何对领域模型划分。战略设计面向业务,偏重于对业务架构的梳理,考虑如何把业务架构和技术架构进行整合。

战术维度关注如何从技术层面进行具体的战略设计的落地。

image.png

战略维度详解

战略维度的两个核心概念为通用语言和限界上下文,其中通用语言对应业务模型中的业务描述,限界上下文对应通用语言中的业务拆分。战略维度决定了系统的架构走向。

image.png

通用语言

通用语言也可以叫做统一协作语言,可以让团队所有人使用同一种语言来描述业务需求,需要业务人员和技术人员统一协作,在意识形态和认知体系上达成一致。通用语言通常不是一步到位,而是需要持续演进的,在这个过程中需要开发人员和业务人员不断沟通,细化业务规则,直到满足系统设计目标为止。

限界上下文

限界上下文承担各个业务功能的集成,准确来说是承担多个子域的功能集成。子域是对领域拆分的切入点,其产生一般取决于系统特征和拆分的需求,一般可以根据核心功能、辅助性功能及第三方功能等特征进行拆分。在微服务中可以认为一个或多个子域对应一个微服务。

限界指对于任何的业务而言,在其边界之内,一个业务概念只有一种特定的含义,不能具有二义性。例如在电商系统中,用户购买上下文中的订单指的是用户下单生成的记录,包含了用户购买的商品,数量价格等信息。 而在商品管理上下文中,订单可能指的是商品的订单记录,包含了商品的销售情况,库存等信息。 两个上下文中都有订单的概念,但是其含义完全不相同,对这个概念的区分会影响后续建模过程中实体的建模。 每个子域都有其限界上下文,通过限界上下文将各个子域整合起来构成完整地领域模型。

子域

子域相对领域而言对应一个更小的问题域或者业务范围,其定位和表现形式也各不相同,基本划分为三种类型的子域,一是核心子域,二是支撑子域,三是通用子域。 核心子域:代表系统中的核心业务逻辑子域,比如电商类应用中订单域负责订单管理相关,归属于核心子域。 通用子域:具有公共能力或者基础设施能力的子域,比如电商类应用中的用户子域负责用户管理等,在各个领域中都会被使用,归属于通用子域。 支撑子域:专注某一方面业务的子域,比如电商类应用中的商品子域,负责商品管理,同时支撑订单域的完整,归属于支撑子域。

防腐层和统一协议

子域拆分之后便有了限界上下文,限界上下文通过集成来构成领域功能。其集成模式有两种,分别是解耦和统一。其中解耦对应防腐层的设计,统一对应统一协议的涉及。 防腐层强调位于下游的限界上下文根据领域模型创建单独的一层组件,该组件完成与上游上下游之间的交互,从而实现业务隔离。一般情况下服务的接口可以充当该服务的防腐层,对Request和Response进行包装。 统一协议指位于上游的限界上下文提供统一的协议,从而促使其他上下文通过统一协议进行访问。 通过防腐层和统一协议的概念可知,处于上游的限界上下文通过统一协议对下游限界上下文的防腐层进行访问,从而完成限界上下文的集成。

战术设计

战术设计则对应业务模型的其他五个维度进行了阐述 其中领域模型对象对应业务对象,领域服务对应业务规则,领域事件代表业务状态,资源库对应业务数据,应用服务对应业务外观。

image.png

领域模型对象

领域模型对象一般分为三大类:聚合、实体及值对象。实体和值对象是聚合的组成部分,值对象同时也是实体组成的一部分。

image.png

实体

实体和值对象的区分在于实体具有两部分的特征,一是唯一标识,二是可变性。 唯一标识是实体对象必须具备的一个属性,通常有多种生成手段,例如随机数,数据库自增列等。 可变性是指实体的状态会随着外界的变化而改变,类比于充血模式,实体具备自己的基础业务方法。

值对象

值对象与实体相比较没有状态,是不可变化的,所以概念上值对象可以进行相等性比较,也可以进行相互替换。 此外值对象由于其不可变性,不对外提供get,set方法,只能通过构造函数进行初始化。 一般值对象可以从实体中梳理而出。例如账户对象,一般账户需要有一个账户id标识唯一性,另外账户中可能会包含地址信息。该实例中账户便是一个实体,地址信息便是一个值对象。账户实体包含地址值对象。

聚合

聚合是DDD中的最核心的领域模型对象,一般来讲系统中的对象之间存在复杂的交互关系,聚合的出现可以降低对象交互的复杂性。

聚合是整理了一组具有相关性的对象的组合。聚合包含两部分,一是聚合根(聚合中的某一个特定的实体对象),二是聚合边界,定义了聚合内部包含的范围。

聚合是数据修改的最小单元,对领域模型对象的修改只能通过聚合跟实现,不能直接通过组合中的任何实体修改, 只有聚合根的实体暴露了对外的操作入口, 对其他对象的任何访问都只能通过聚合内部的遍历才能访问。删除操作也必须一次性删除聚合内的所有对象,通过这种严格的边界定义可以降低对象之间的交互次数。

对于聚合的设计一般有以下三个指导原则 一是一次事物操作只修改一个聚合实例。 二是尽量设计小聚合,大聚合内部包含的实体和值对象相对较多,并且处理过程相对较麻烦。 三是聚合之间通过唯一标识引用而非通过对象引用。

领域服务

image.png 现实中很多业务操作需要跨聚合相互协作才能完成,领域服务提供了这种协作的入口。例如转账服务。势必涉及到订单Order及账户Account等聚合的相互操作。封装这两个聚合的操作的Service便是领域服务。 领域服务实际上就是跨聚合的交互,期输入则是各个聚合的根实体对象,输出往往是一个无状态的值对象。

领域事件

把领域中所发生的活动建模成一系列的事件。领域事件的生命周期有4个阶段,分别是生成、存储、分发及消费。 一般情况下当一个实体依赖于另一个实体但是两者之间又不想产生强耦合时,我们可以提取领域事件。 例如电商系统中生成订单之后可能需要给用户发送一个通知,为了避免订单和通知产生强关联,当订单生成时发布一个订单已被创建的时间,由通知模块订阅该事件。

领域事件一般使用聚合对象名+动作的过去式对事件进行命名,领域事件一般必须要包含事件的唯一标识、产生时间、及事件来源等元数据,当然也可以定义自己需要的业务数据。

事件具有严格的不变性,在任何场合都不能对事件本身进行修改,因为事件代表的是一种瞬时状态,该状态已发生。 事件的处理一般都是发布-订阅模式,领域事件既可以由本地限界上下文消费,也可以由远程限界上下文消费。

资源库

image.png 资源库为应用程序提供统一的数据访问入口。资源库屏蔽了数据访问的技术复杂性和其差异性。例如对NewSQL,传统sql访问的差异性。

应用服务

应用服务是DDD中唯一一个与用户界面直接交互的组件, 可以根据是否修改聚合对象将应用服务分为两大类,一类是查询服务,一类是命令服务,同时于此衍生出两种对象,一是查询对象,一是命令对象。这两个对象也属于领域对象。

基础设施

基础设施是针对各种资源库的具体实现,并且其中可以存在各种针对领域事件的消息中间件以及系统中各种配置管理,工具服务等都属于基础设施组件。

DDD中常用包结构管理

image.png

根据以上战术设计对应的代码包结构划分 包结构可以根据侧重点不同划分为两类,一类是侧重于限界上下文内部的组件管理,另一类则关注与限界上下文之间的交互,其中domain、application、infrastructure属于限界上下文内部的组件管理,interface和integration属于限界上下文之间的交互管理。

domain包含所有领域模型对象和领域事件对象。因此可分为两个层级,model和domain,其中model层放领域模型对象包括聚合、实体及值对象。domian层放领域事件对象包括repository、event、command及query。其中repository是对资源库的定义而非实现,具体的实现位于infrastructure层。evnet存放事件对象,commond和query对应命令和查询对象。

application包存放应用服务,应用服务是限界上下文中领域模型的外观,提供向底层领域模型发送命令和查询对象的服务。其基于命令和查询机制,分为commondService和queryService。

infrastructure层主要负责repository的具体实现和messaging的消息通信机制。另外也可存放其他底层技术组件。

interface层负责将所有入站操作封装到当前的限界上下文中。主要用途是对外暴露接口及响应外来领域事件。在对外接口中一般需要将外界入参DTO通过各个Assember转换为command对象和query对象。

integration包提供数据的出口,负责对外发送event事件及封装远程请求acl。integration包可以看做是一种防腐层。

当然,这些包结构划分没有具体的定论一定要按照这种规则进行划分,在某些简单的业务中,可以视业务复杂程度删减相对应的包划分。而不是生搬硬套这种模式。