1.简介
领域驱动设计是一种复杂软件系统建模与设计方法论。
DDD 的核心思想是通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性。
DDD 不仅可以用于微服务设计,还可以很好地应用于企业中台的设计,也适用于传统的单体应用。
本篇文章主要介绍 DDD 的相关概念,让初学者对 DDD 有一个基础认知。
2.前世今生
领域驱动设计(Domain-Driven Design,DDD)最早由程序员 Eric Evans 于 2003 年在他的同名书籍 《Domain-Driven Design: Tackling Complexity in Software》 中提出的概念。
该书翻译过来就是《领域驱动设计—软件核心复杂性应对之道》,但是提出的时候微服务当时并没有流行,所以一直没有火起来,DDD 最近开始流行的原因,主要是借着微服务的东风。
Martin Fowler 于 2014 详细阐述了微服务架构,随后微服务架构逐渐兴起。
DDD 随着微服务架构的流行而火起来,主要是因为两者在设计理念、服务划分、复杂性管理、团队协作等方面存在天然的契合。DDD 提供的业务驱动设计和领域建模方法论为微服务架构的有效实施提供了理论支持,使得开发团队能够更好地管理复杂性,快速响应业务变化。
随着企业对灵活性和可扩展性的需求不断增长,很多大型互联网企业已经将 DDD 设计方法作为微服务的主流设计方法了。
3.DDD 的作用
DDD 主要目标是解决复杂业务系统中的设计难题,使软件设计与业务需求紧密对齐,从而提高系统的适应性、可维护性和开发效率。
以下是需要采用 DDD 的主要原因:
- 解决复杂业务问题
现代软件系统通常需要处理高度复杂的业务逻辑。简单的 CRUD(创建、读取、更新、删除)设计难以有效应对这种复杂性。
DDD 通过领域建模,将复杂业务拆解为多个明确的领域和上下文,用统一语言和模型来描述问题,从而降低复杂性。
- 促进业务与技术协作
开发团队和业务团队之间经常缺乏共同语言,导致需求误解和实现偏差。
DDD 通过构建统一语言(Ubiquitous Language),让业务和技术团队使用一致的术语和概念进行沟通。
将业务知识直接嵌入领域模型中,确保软件正确反映业务需求。
- 增强系统的可维护性
传统设计方式容易导致代码耦合严重,系统扩展和维护成本高。
DDD 使用限界上下文(Bounded Context)将系统分解为多个独立模块,降低模块之间的依赖。
通过明确领域模型,确保每个模块的设计围绕其专注的业务目标。
- 适应业务的快速变化
在动态的市场环境中,业务需求经常变化,传统设计方法难以快速响应。
DDD 使用模块化设计,让系统可以针对特定的业务变化进行独立调整,而不影响其他模块。
聚焦核心子领域(Core Subdomain),确保开发资源优先投入到具有战略价值的部分。
- 应对分布式系统的复杂性
微服务架构中,各服务的职责不清、模型不一致会导致问题。
DDD 定义清晰的上下文边界,帮助划分微服务的职责。
确保每个服务只关注其领域的核心逻辑,减少跨服务耦合。
通过引入 DDD,软件开发团队可以更有效地管理复杂性,确保技术实现与业务需求一致,构建高质量、可持续发展的软件系统。
4.为什么 DDD 适合微服务?
微服务的边界问题
微服务架构是一种将应用程序拆分为小的、独立的服务的架构模式,每个服务可以独立开发、部署和扩展。
尽管微服务架构带来了许多好处,如提高灵活性、可扩展性和便于团队协作,但在实施和管理微服务时,也会面临一些困境和挑战。
微服务设计,面对的第一个问题是微服务到底怎么拆分和设计才算合理,拆多小才叫微服务?。
微服务边界的划分 历来也是最容易产生争议的地方。
比如面对一个复杂的业务系统,应该设计多少个微服务,如何确定每个微服务的职责、粒度和边界比较合适,避免随着业务的发展,导致微服务耦合严重,复杂臃肿,难以维护,进而导致后续不必要的重构和微服务拆分。
DDD 的战略设计和战术设计
DDD 是一种处理高度复杂领域的设计思想 ,它试图分离技术实现的复杂性,并围绕业务概念构建领域模型来控制业务的复杂性,以解决软件难以理解,难以演进的问题。DDD 不是架构,而是一种架构设计方法论 ,它通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现架构演进。
DDD 包括战略设计和战术设计两部分。
-
战略设计主要从业务视角出发 ,建立业务领域模型,划分领域边界,建立通用语言的限界上下文。限界上下文可以作为微服务设计的参考边界。
-
战术设计则从技术视角出发 ,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、领域事件和资源库等代码逻辑的设计和实现。
DDD 战略设计会建立领域模型 ,领域模型可以用于指导微服务的设计和拆分 。
事件风暴是建立领域模型的主要方法,它是一个从发散到收敛的过程。它通常采用用例分析、场景分析和用户旅程分析,尽可能全面不遗漏地分解业务领域,并梳理领域对象之间的关系,这是一个发散的过程。事件风暴过程会产生很多的实体、命令、事件等领域对象,我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型,这就是一个收敛的过程。
如何确定微服务的边界?
我们可以用三步来划定领域模型和微服务的边界。
我们可以用三步来划定领域模型和微服务的边界。
-
第一步:在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。
-
第二步:根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。在上面的图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线表示。
-
第三步:根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型。在这个图里,限界上下文之间的边界是第二层边界,这一层边界可能就是未来微服务的边界,不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行,物理上相互隔离,所以是物理边界,边界之间用实线来表示。
有了这两层边界,微服务的设计就不是什么难事了。
在战略设计中我们建立了领域模型,划定了业务领域的边界,建立了通用语言和限界上下文,确定了领域模型中各个领域对象的关系。到这儿,业务端领域模型的设计工作基本就完成了,这个过程同时也基本确定了应用端的微服务边界。
在从业务模型向微服务落地的过程中,也就是从战略设计向战术设计的实施过程中,我们会将领域模型中的领域对象与代码模型中的代码对象建立映射关系,将业务架构和系统架构进行绑定。当我们去响应业务变化调整业务架构和领域模型时,系统架构也会同时发生调整,并同步建立新的映射关系。
DDD 与微服务的关系?
DDD 是一种架构设计方法,微服务是一种架构风格。
两者都强调从业务出发,合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力,也就是我们常说的演进式架构。
二者的区别也很明显:
-
DDD 主要关注:从业务领域视角划分领域边界,构建通用语言进行高效沟通,通过业务抽象,建立领域模型,维持业务和代码的逻辑一致性。
-
微服务主要关注:运行时的进程间通信、容错和故障隔离,实现去中心化数据管理和去中心化服务治理,关注微服务的独立开发、测试、构建和部署。
通过 DDD 战略设计可以建立领域模型,划定领域边界,解决微服务设计过程中,边界难以划定的难题。如果你的业务焦点在领域和领域逻辑,那么你就可以选择 DDD 作为微服务的设计方法!
5.基本概念
DDD 的知识体系提出了很多的名词,像:
- 领域
- 子域
- 核心域
- 通用域
- 支撑域
- 通用语言
- 限界上下文
- 聚合
- 聚合根
- 实体
- 值对象
- 领域服务
- 领域事件
等等这些名词,都是关键概念,但它们实在有些晦涩难懂,可能导致你还没开始实践 DDD 就打起了退堂鼓。
所以理解这些概念是入门 DDD 的前提,可以先看下面这张图。
关于 DDD 的相关的概念,可以参考 DDD 概念参考 - 领域驱动设计。
5.1 领域分析类概念
领域(Domain)
领域指的是问题所在的业务背景或知识范围。
领域是用来确定范围的,范围即边界,在 DDD 中一直在强调边界,就是这个原因。
例如,在银行的应用程序就是一个特定领域,该领域涉及帐户、交易和相关银行法规等概念。
领域是业务价值的来源,也是软件设计的核心。
子域(Sub Domain)
领域可以进一步划分为子领域。
我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。
在采用领域驱动设计(DDD)构建银行应用程序时,可以将系统划分为多个子域,每个子域对应特定的业务职能。
- 账户管理(Account Management),负责账户的生命周期管理,包括创建、更新、关闭等操作。
- 交易与支付(Payments and Transfers),处理资金在账户之间的转移。
- 贷款管理(Loan Management),管理个人和企业贷款产品。
- 风险管理(Risk Management),评估和管理金融风险。
- 投资与财富管理(Investment and Wealth Management),为客户提供投资、理财和资产管理服务。
- 客户管理(Customer Management),维护客户数据并管理客户关系。
领域的核心思想就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。通过领域细分,逐步缩小服务需要解决的问题域,构建合适的领域模型。
领域模型映射成系统就是微服务了。
核心域、通用域和支撑域
在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域 ,它们分别是:核心域、通用域和支撑域。
- 核心域(Core Subdomain)
决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。
- 通用域(Generic Subdomain)
没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。
- 支撑域(Supporting Subdomain)
还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。
这三类子域相较之下,核心域是最重要的。
核心域直接体现了企业在市场中的独特业务价值,例如银行的支付结算系统、电商的推荐算法。如果核心域的设计失败,系统可能无法实现业务目标,进而影响企业的竞争优势。
通用域和支撑域如果对应到企业系统,举例来说的话,通用域则是你需要用到的通用系统,比如认证、权限等等,这类应用很容易买到,没有企业特点限制,不需要做太多的定制化。而支撑域则具有企业特性,但不具有通用性,例如数据代码类的数据字典等系统。
核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。
通用语言与限界上下文
在 DDD 领域建模和系统建设过程中,有很多的参与者,包括领域专家、产品经理、项目经理、架构师、开发经理和测试经理等。对同样的领域知识,不同的参与角色可能会有不同的理解,那大家交流起来就会有障碍,怎么办呢?因此,在 DDD 中就出现了 通用语言 和 限界上下文 这两个重要的概念。
这两者相辅相成,通用语言定义上下文含义,限界上下文则定义领域边界, 以确保每个上下文含义在它特定的边界内都具有唯一的含义,领域模型则存在于这个边界之内。
通用语言
怎么理解通用语言(Ubiquitous Language)这个概念呢?
在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。也就是说,通用语言是团队统一的语言,不管你在团队中承担什么角色,在同一个领域的软件生命周期里都使用统一的语言进行交流。
通用语言可以解决交流障碍这个问题,使领域专家和开发人员能够协同合作,从而确保业务需求的正确表达。
但是,对这个概念的理解,到这里还不够。
通用语言包含术语和用例场景,并且能够直接反映在代码中。通用语言中的名词可以给领域对象命名,如商品、订单等,对应实体对象;而动词则表示一个动作或事件,如商品已下单、订单已付款等,对应领域事件或者命令。
通用语言贯穿 DDD 的整个设计过程。作为项目团队沟通和协商形成的统一语言,基于它,你就能够开发出可读性更好的代码,将业务需求准确转化为代码设计。
图描述了从事件风暴建立通用语言到领域对象设计和代码落地的完整过程。
- 事件风暴的过程中,领域专家会和设计、开发人员一起建立领域模型,在领域建模的过程中会形成通用的业务术语和用户故事。事件风暴也是一个项目团队统一语言的过程。
- 通过用户故事分析会形成一个个的领域对象,这些领域对象对应领域模型的业务对象,每一个业务对象和领域对象都有通用的名词术语,并且一一映射。
- 微服务代码模型来源于领域模型,每个代码模型的代码对象跟领域对象一一对应。
限界上下文
我们知道语言都有它的语义环境,同样地,通用语言也有它的上下文环境。
为了避免同样的术语在不同的上下文环境中产生歧义 ,DDD 在战略设计上提出了 限界上下文(Bounded Context) 这个概念,用来确定术语所在的领域边界。
官方解释:限界上下文主要用来封装通用语言和领域对象。
我们可以将限界上下文拆解为两个词:限界和上下文
- 限界就是领域的边界
- 而上下文则是语义环境
通过领域的限界上下文,我们就可以在统一的领域边界内用统一的语言进行交流。
限界上下文和微服务的关系
理论上限界上下文就是微服务的边界。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案。
可以说,限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。
不过,这里还是要提示一下:除了理论,微服务的拆分还是有很多限制因素的,在设计中不宜过度拆分。
5.2 领域建模类概念
领域模型
领域模型(Model)是业务概念在程序中的一种表达方式。
领域模型可以用来设计和理解整个软件结构。面向对象设计中的类概念是领域模型的一种表达方式。与此类似,UML的建模方法也可以应用在对领域模型的表达上。
在 DDD 实践中,领域模型应当尽量简洁,能反应业务概念即可。
应用服务
应用服务(Application Service) 主要用于协调和管理应用程序的业务逻辑,同时充当用户界面(UI)与领域模型(Domain Model)之间的桥梁。应用服务负责接收用户请求、与领域服务和其他组件进行交互、执行特定的用例,并返回结果。
跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。
应用服务的特点:
- 协调者: 应用服务通常充当一个协调者,负责调用多个领域服务、实体或其他应用服务来完成特定的业务用例。
- 无业务逻辑: 应用服务本身通常不包含复杂的业务逻辑,而是将这些逻辑委托给领域服务或领域模型。它主要负责处理应用程序的流程。
- 处理事务: 应用服务可以管理事务的生命周期,确保在执行过程中数据的一致性和完整性。
- 接口导向: 应用服务通常定义为接口,这样可以更容易地进行测试和替换实现。
领域服务
领域服务(Domain Service)主要用于处理那些不适合放在实体(Entity)或值对象(Value Object)中的业务逻辑。
领域服务代表领域中的一些操作或业务逻辑,它不属于特定的实体或值对象,而是为封装跨多个实体或值对象的业务逻辑。
领域服务有如下特点:
- 无状态: 领域服务通常是无状态的,这意味着它们不持有任何持久化的数据。它们的功能主要依赖于输入参数,而不是内部状态。
- 聚焦于业务逻辑: 领域服务专注于处理特定的业务逻辑,这些逻辑可能涉及多个实体或值对象,但不适合放置在某个特定实体中。
- 接口导向: 领域服务通常通过接口定义,这使得它们更易于测试和替换。可以使用依赖注入(Dependency Injection)来实现服务的灵活性。
- 跨领域对象工作: 领域服务可以协调多个实体或值对象之间的交互,处理复杂的业务操作。
实体和值对象
实体和值对象都是领域模型中的领域对象。
实体(Entity)
在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,
在特定领域里,拥有拥有唯一标识符、可自我验证的、拥有行为的对象,被称为实体。
实体重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。
实体属于充血模型。
(1)实体的业务形态
在 DDD 不同的设计过程中,实体的形态是不同的。
在战略设计时,实体是领域模型的一个重要对象。领域模型中的实体是多个属性、操作或行为的载体。 在事件风暴中,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。你可以这么理解,实体和值对象是组成领域模型的基础单元。
(2)实体的代码形态
在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。
(3)实体的运行形态
实体以领域对象的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。
(4)实体的数据库形态
与传统数据模型设计优先不同,DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为 ,再将实体对象映射到数据持久化对象。
在领域模型映射到数据模型时,一个实体可能对应 0 个、1 个或多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。
在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,它不需要持久化。比如,基于多个价格配置数据计算后生成的折扣实体。
而在有些复杂场景下,实体与持久化对象则可能是一对多或者多对一的关系。比如,用户 user 与角色 role 两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。再比如,有些场景为了避免数据库的联表查询,提升系统性能,会将客户信息 customer 和账户信息 account 两类数据保存到同一张数据库表中,客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景。
值对象
值对象(Value Object)是通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。
实体可以使用 ID 标识,但是值对象是用属性标识,任何属性的变化都被视为产生了一个新的值对象。
值对象
(1)值对象的业务形态
值对象是 DDD 领域模型中的一个基础对象,它跟实体一样都来源于事件风暴所构建的领域模型,都包含了若干个属性,它与实体一起构成聚合。
我们不妨对照实体,来看值对象的业务形态,这样更好理解。本质上,实体是看得到、摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。而值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。
在值对象中也有部分共享的标准类型的值对象,它们有自己的限界上下文,有自己的持久化对象,可以建立共享的数据类微服务,比如数据字典。
(2)值对象的代码形态
值对象在代码中有这样两种形态。
如果值对象是单一属性,则直接定义为实体类的属性;如果值对象是属性集合,则把它设计为 Class 类,Class 将具有整体概念的多个属性归集到属性集合,这样的值对象没有 ID,会被实体整体引用。
我们看一下下面这段代码,person 这个实体有若干个单一属性的值对象,比如 Id、name 等属性;同时它也包含多个属性的值对象,比如地址 address。
// Person 有主键ID实体。
type Person struct {
Id string // 单一属性值对象
Name string // 单一属性值对象
Age int // 单一属性值对象
Gender bool // 单一属性值对象
Addr Address // 属性集值对象,被实体引用
}
// Address 无主键ID值对象
type Address struct {
Province string //值对象
City string //值对象
County string //值对象
Street string //值对象
}
(3)值对象的运行形态
实体实例化后的领域对象(Domain Object)的业务属性和业务行为非常丰富,但值对象实例化的对象则相对简单和乏味。除了值对象数据初始化和整体替换的行为外,其它业务行为就很少了。
值对象嵌入到实体的话,有这样两种不同的数据格式,也可以说是两种方式,分别是属性嵌入和序列化大对象。
案例 1:以属性嵌入的方式形成的人员实体对象,地址值对象直接以属性值嵌入人员实体中。
案例 2:以序列化大对象的方式形成地址值对象,地址值对象被序列化成大对象 Json 串后,嵌入人员实体中。
(4)值对象的数据库形态
DDD 引入值对象是希望实现从 数据建模为中心 向 领域建模为中心 转变,减少数据库表的数量和表与表之间复杂的依赖关系,尽可能地简化数据库设计,提升数据库性能。
如何理解用值对象来简化数据库设计呢?
传统的数据建模大多是根据数据库范式设计的,每一个数据库表对应一个实体,每一个实体的属性值用单独的一列来存储,一个实体主表会对应 N 个实体从表。而值对象在数据库持久化方面简化了设计,它的数据库设计大多采用非数据库范式,值对象的属性值和实体对象的属性值保存在同一个数据库实体表中。
在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。
(5)值对象的优势和局限
值对象是一把双刃剑,它的优势是可以简化数据库设计