在学习了 DDD(领域驱动设计,Domain Driven Design)后,对 DDD 这一领域建模方法论有了更深的理解。不过与其说是方法论,笔者更认为是一种系统设计方面的指导思想,其价值在于,即便不进行全面的 DDD 重构,依然能借鉴其核心原则,对现有系统进行精准而有效的优化。
在经历了单体应用、微服务/分布式系统的研发后,笔者见到很多业务系统在需求的快速迭代下,最终都陷入了代码野蛮生长、模块边界模糊、微服务名存实亡的困境。倘若从项目伊始,便能遵循一种统一的设计哲学去建模与实现,构建一个灵活且可持续演进的架构,那么在应对层出不穷的需求变更时,无疑会更加从容和优雅。
然而 DDD 的成功落地并非易事,它对团队的综合素养提出了较高的要求。从构建通用语言、精炼领域模型,到最终的代码实现,每一个环节都考验着团队对业务本质与 DDD 思想的深刻理解。若缺乏这份理解便匆忙动手,忽视其背后的「战略设计」思想,便极易陷入“为了 DDD 而 DDD”的技术陷阱,其结果往往是创造出一个披着 DDD 外衣的传统三层架构。
本文旨在梳理学习 DDD 过程的笔记,记录各个核心知识点的理解,而真正的应用还是要在日常研发中多从 DDD 的思想角度出发,去思考、设计与实践。
笔者主要阅读的是《中台架构与实现:基于DDD和微服务》,此书笔者觉得比较适合国人阅读,其从零开始,对 DDD 的各个概念都有讲解,并搭配业务案例以 DDD 实现,能更好的理解 DDD 对系统设计带来的改变和好处。后两本是 DDD 领域的红蓝皮书,是 DDD 概念和实战方面的权威之作,可以搭配着阅读。
以上四个视频都由美团技术团队发布,侧重于 DDD 的实践,可以在理解了 DDD 的核心知识后观看。
领域(Domain):即特定的范围或区域。DDD 中会对业务进行领域(范围)划分,然后将问题限定在特定的边界内,在该边界内建立领域模型,从而解决相应业务问题。简言之,DDD 领域即边界内要解决的业务问题域。
子域:领域的划分,会形成不同子域,分为以下三类:
事件风暴工作坊(Event Storming Workshop):以 DDD 为核心,基于事件(Event)的协作式建模技术。通过识别和排序业务领域中发生的「领域事件」来揭示业务流程、发现聚合、实体和限界上下文。
通用语言(Ubiquitous Language):定义上下文对象的含义,在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确地描述业务含义和规则的语言。
限界上下文(Bounded Context):确定领域边界,确保上下文对象在特定的边界内具有唯一含义,以便组合这些对象构建领域模型。限界上下文定义了一个没有语义二义性的业务边界,这个边界既是业务领域的边界,也是微服务拆分的逻辑边界。限界上下文之间也存在映射关系,如下:
用户接口层:负责与外部系统(用户)交互,接受请求和展示响应数据,这里的用户可能是用户、程序、自动化测试和批处理脚本等。该层的存在使得面对不同的前端应用时,能灵活适配不同业务的需求(数据适配),并且不影响核心业务逻辑(领域层、应用层)。该层主要有以下内容:
应用层:连接用户接口层和领域层,主要职能是协调领域层多个聚合(领域服务),面向业务流程完成服务的组合和编排。
领域层:实现领域模型的核心业务逻辑。这一层聚集了领域模型的聚合、聚合根、实体、值对象、领域服务和事件等领域对象,通过各领域对象的协同和组合形成领域模型的核心业务能力。
基础层:为其他各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。常见的功能是完成实体的数据库持久化(仓储实现)。
领域模型(Domain Model)是特定业务领域的抽象表示,是业务的核心逻辑和规则的体现。其由多种基础领域对象(Domain Object,DO)构成,而领域对象持久化到数据库时,名称和状态可能发生变化,此时会将领域对象转化为持久化对象(Persistent Object,PO)。
实体(Entity):当对象具有唯一标识符,并且标识符在历经各种状态变更后仍能保持一致的即为实体。实体即业务对象,集业务属性、行为为一体;在代码上表现为实体类,类中有属性和方法,与实体相关的业务逻辑都在类中方法实现(充血模型)。
值对象(Value Object,VO):是通过对象属性值来识别的对象,它将多个不可修改的相关属性组合为一个概念整体,用于描述领域的某个特定方面,并且是一个没有标识符的对象。在持久化时,值对象不会作为单独的数据表存在,而是属于实体表的一个 JSON 字段(对象)或冗余的列(对象字段,不推荐),以此优化数据表设计和减少联表操作。
聚合(Aggregate):由业务和逻辑紧密关联的实体和值对象组成。聚合定义了一个事务(一致性)边界,聚合内数据的修改必须由聚合根同一组织,聚合是数据修改和持久化的基本单元。
聚合根(Aggregate Root):又称根实体,聚合根是实体;同时在聚合内负责协调实体和值对象,按照固定的业务规则,协同完成聚合共同的业务逻辑;并且是聚合对外的访问对象,聚合之间以聚合根 ID 关联的方式接受聚合的外部请求。但聚合外部对象不能直接通过对象引用的方式访问聚合内的对象,而应当通过应用服务调用。
领域服务(Domain Service)属于领域层,主要负责封装核心业务逻辑,特别是当一个业务逻辑需要同一个聚合内的多个实体或实体方法协同完成时;它处理的是单个聚合内部无法由实体自身完成的复杂业务行为,若一个业务场景需要同一个聚合内的 2~n 个实体共同完成,这段业务逻辑就可以用领域服务来组合两个实体完成。
之所以不用领域服务来组合多个聚合的协同,是为了实现聚合的高内聚,防止领域服务变得臃肿;其次是为了避免架构演进时,聚合拆分重组出现的问题,如同一限界上下文内的多个聚合拆分为多个微服务导致的引用依赖问题。因此跨聚合组合的行为应该交给应用服务。
领域事件(Domain Event)表示领域中发生的事件,该事件会导致进一步的业务操作,在实现领域模型解耦的同时,还有助于形成完整的业务操作闭环。
领域事件采用事件驱动架构(Event-Driven Architecture,EDA)设计,可切断领域模型之间的强依赖关系,在事件发布后,发布方无需关系订阅者是否处理成功。
领域事件一般结合消息中间件和事件发布订阅的异步处理方式,实现数据最终一致性。采用最终一致性是因为基于聚合的一个设计原则:在边界之外使用最终一致性。
领域事件可以发生在微服务的聚合之间,或微服务之间。
领域事件至少包含:事件唯一标识(全局 ID)、发生时间、事件源、事件发生背景相关的业务数据。
应用服务(Application Service)属于应用层,跨多个聚合的业务逻辑的组合与编排,通过应用服务来实现;不同聚合之间的协同操作要注意入参尽量通过 ID、参数方式传递,而不是直接传递引用对象(避免依赖问题)。
充血模型(Rich Domain Model):在充血模型中,业务逻辑都在领域实体对象中实现,实体本身不仅包含了属性,还包含了它的业务行为。
贫血模型(Anemic Domain Model):此类领域对象大多只有 setter/getter 方法,业务逻辑统一放在业务逻辑层实现,而不是在领域对象中实现;贫血领域对象(Anemic Domain Object)就是指仅用作数据载体,而没有行为和动作的领域对象,简单说就是只有属性没有任何函数的类。
失血模型:比贫血模型更糟糕的一种情况,不仅领域对象缺乏业务行为,其对应的业务逻辑层也未能清晰表达业务流程,更多只是对数据持久层方法的调用。
胀血模型:指过度膨胀的充血模型,在该模型下的领域对象,通常包括了不属于实体自身职责的属性与业务逻辑,导致实体变得过于庞大复杂,职责不清晰。
DDD 分层架构依赖原则:每层只能与位于其下方的层发生耦合。
根据耦合的紧密程度可以分为两种架构模式:
在传统松散分层架构,由于能够调用任意下方的层级,会导致职责混淆。如:
在严格分层架构,用户接口层 -> 应用层 -> 领域层是单向依赖,使得每层职责清晰。依赖倒置下,高层模块不依赖低层模块,两者都依赖抽象。如:
.
├── interfaces
│ ├── assembler
│ ├── dto
│ └── facade
├── application
│ ├── event
│ │ ├── publish
│ │ └── subscribe
│ └── service
├── domain
│ ├── aggregate1
│ │ ├── entity
│ │ ├── event
│ │ ├── repository
│ │ └── service
│ └── aggregate2
│ │ ├── ... ...
│ └── ... ...
└── infrastructure
├── config
└── util
├── dao
├── api
├── driver
├── eventbus
├── mq
└── ... ...
event(事件)
service(应用服务)
aggregate1(聚合)
aggregate2(聚合):其它聚合