DDD学习笔记

本文围绕领域驱动设计(DDD)展开,介绍了其概念、核心知识体系、分层架构,分析了面临的挑战,探讨了与微服务的关系、易学难精的原因等。DDD将业务逻辑作为核心,分战略和战术设计,有独特的分层架构,但在认知、语言维护等方面存在挑战。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. DDD是什么

DDD(Domain-Driven Design,领域驱动设计)是一种将复杂的业务逻辑作为软件开发的核心,通过深入理解业务领域,构建高质量软件的方法论。整体上,DDD分为战略设计和战术设计两个层次。

2. DDD的核心知识体系

战略设计

解决宏观架构问题,关注点在于如何划分业务领域,确定各个领域的边界和相互关系,以及如何组织和协调这些领域内的工作
战略设计的关键任务包括:

  1. 领域划分:识别业务的子领域(Subdomains),确定各子领域的核心价值和优先级。
  2. 识别限界上下文(Bounded Contexts):明确每个子领域的工作范围和规则,防止领域模型的混乱和冲突。
  3. 统一语言(Ubiquitous Language):在团队中建立一种共享的、精准反映业务含义的语言,用于沟通和建模。
  4. 上下文映射(Context Mapping):处理不同上下文之间的交互和边界,比如通过共享内核、防腐层、客户/供应商关系等模式来集成多个上下文。

战术设计

关注于在特定上下文中如何设计和实现领域模型
战术设计的关键任务包括:

  1. 领域模型细化:定义实体(Entities)、值对象(Value Objects)、聚合(Aggregates)、领域服务(Domain Services)等具体模型元素,明确它们的职责和关系。
  2. 模块化与分层架构:在代码层面划分模块,实现领域模型的模块化和分层设计,如应用层、领域层、基础设施层等。
  3. 对象生命周期管理:设计对象如何创建、修改、查找、删除等生命周期管理机制,如使用工厂模式创建实体,仓储模式(Repositories)管理持久化。
  4. 事务脚本、领域服务、领域事件等设计模式:根据业务需求选择合适的领域设计模式来处理业务逻辑。

领域(Domain)

DDD的核心概念之一,它是业务世界的某个特定部分,通常指的是某一类业务问题的集合,即软件系统所解决的问题空间。
领域可以进一步划分为子领域,每个子域对应一个更小的问题域或更小的业务范围,是个递归的概念。
子域可以根据自身重要性和功能属性划分为三类子域:

  1. 核心域(Core Domain):业务中最关键、最具竞争力的部分,是组织业务的独特之处,也是软件系统最重要的组成部分。核心域直接影响着业务的成功与否,因此应投入最多的资源进行开发和维护。
  2. 通用域(Generic Domain):具有一定通用性,并非竞争优势所在,但也是必不可少的辅助功能的部分。通常可以复用现有的成熟解决方案或框架。如用户管理、权限管理等模块。
  3. 支撑域(Supporting Domain):是那些支持核心域和通用域运行的辅助功能模块。支撑域虽然不直接产生业务价值,但是对保证业务的合规性和可持续性有着重要作用。如日志、财务等模块。

界限上下文(Bounded Context)

定义了一个明确的边界,在这个边界内部,领域模型(包括业务规则、数据结构和操作)具有无二义性的定义,并且在这个边界内具有权威性。

  1. 限界上下文定义了领域模型的应用范围:每一个限界上下文对应一个特定的业务领域子集,这个子集有自己的模型和一套完整的业务规则,与其他限界上下文相互独立,从而避免了在一个大一统的模型中可能出现的语义混乱和复杂性爆炸。
  2. 上下文内的通用语言:在每个限界上下文中,团队成员(包括开发者和领域专家)共同采用一种通用语言(Ubiquitous Language),这种语言既用于业务沟通也用于技术沟通,甚至于代码编写,确保团队在处理同一业务问题时有一致的理解。
  3. 上下文之间的关系:不同的限界上下文之间可能存在各种交互关系,比如客户-供应商关系(Customer-Supplier)、共享内核(Shared Kernel)、防腐层(Anticorruption Layer)等。这些关系描述了上下文间如何协作和集成,同时防止它们之间的耦合过度,保持各自模型的纯洁性。

统一语言(Ubiquitous Language)

是一个极其重要的概念,它是一种共享语言,旨在促进领域专家和技术团队之间的有效沟通。统一语言基于业务领域特有的词汇和概念,将业务领域知识转化成一套简洁且一致的词汇表和概念模型,从而减少沟通中的误解和误差,增强团队成员对业务领域模型的共识。
统一语言的作用包括:

  1. 澄清业务概念:通过定义和规范业务领域内的关键词汇和术语,确保团队成员对业务问题的理解和表达方式一致。
  2. 指导代码设计:统一语言不仅用于口头和书面沟通,也应用于代码和数据库设计中,使得代码结构、类名、方法名和数据库表结构等都能够反映出真实的业务含义,提高了代码的可读性和可维护性。
  3. 增进团队协作:通过共享的统一语言,业务专家和技术人员可以更有效地讨论和理解复杂的业务需求,加速设计和开发进程,减少返工。

实体(Entity)

具有唯一标识符的对象,它代表了业务领域中的一个具体事物。
实体通常具备以下特征:

  1. 唯一标识符:每个实体都有一个独一无二的标识,用于在整个系统中明确地标识和引用实体。
  2. 生命周期:实体有自己的生命周期,可以经历创建、更新和删除等操作。
  3. 行为和状态:实体不仅包含属性,还可以拥有与业务逻辑相关的操作(方法),反映了实体在领域中的行为和状态变化能力。

值对象(Value Object)

没有唯一标识符的对象,它代表了业务领域中的一个具体属性。
值对象的主要特征包括:

  1. 完整性:值对象的属性是不可变的,一旦创建完成,其内部状态就不能被修改。若要更改其状态,必须创建一个新的值对象实例。
  2. 相等性:值对象之间的相等性基于其内容(属性值),而不是基于对象的身份标识。也就是说,如果两个值对象的所有属性值都相同,则认为它们是相等的。
  3. 依赖于属性:值对象的定义和意义完全依赖于其内在的属性值,而不具备独立的身份标识。

聚合(Aggregate)

由业务和逻辑紧密关联的实体和值对象组合而成的对象,这些对象作为一个不可分的单元,来处理业务规则和数据一致性
聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
聚合具有以下几个关键特性:

  1. 聚合根(Aggregate Root):每个聚合都有一个称为聚合根的实体,它是外部对象访问和修改聚合内部状态的唯一入口。聚合内的所有操作都应该通过聚合根来完成。
  2. 封装性:聚合内部的实体和值对象形成了一个封闭的边界,对外部世界隐藏了内部实现细节。所有对聚合状态的修改必须通过聚合根提供的方法进行,以确保数据一致性。
  3. 一致性保证:聚合设计的目标之一是在事务边界内确保业务规则得以执行,从而保持数据的一致性。
  4. 生命周期管理:聚合根通常负责聚合内所有成员的生命周期管理,包括创建、更新和删除操作。

领域服务(Domain Service)

用来封装那些不属于任何特定实体或值对象的责任,而是在整个领域模型中扮演核心角色,执行跨越多个实体或值对象的业务逻辑,它反映了领域内难以通过实体或值对象自然表达出来的操作或流程。
领域服务的特点包括:

  1. 封装复杂业务逻辑:当领域中的某个业务操作涉及多个实体协作或需要额外的业务规则判断,这些操作不适合放在任何一个实体中时,就可以定义一个领域服务来执行这个操作。
  2. 无状态:领域服务通常是无状态的,也就是说,它不包含任何实例变量,每次调用时只关注传入的参数,并基于参数执行相应的业务逻辑。
  3. 协调多个对象:领域服务能够协调领域内的多个对象一起完成某项任务,特别是一些跨越多个聚合的操作。
  4. 领域逻辑的载体:对于那些难以归入实体或值对象,但又非常重要且与领域密切相关的操作,领域服务是一个很好的承载者。

仓储(Repository)

抽象数据存储,隐藏底层数据库操作细节,提供面向领域对象的查询和持久化操作。
仓储的主要作用和特点包括:

  1. 隔离持久化细节:仓储模式将数据访问和领域模型分离,使得领域模型不受数据库表结构、SQL查询语句等持久化细节的影响,保持领域模型的纯净性。
  2. 简化领域对象的管理:仓储提供了对领域对象集合(如实体列表)的抽象视图,可以方便地进行添加、删除、更新和查询操作,如同操作内存中的对象一样。
  3. 支持统一语言:仓储的接口设计应遵循统一语言的原则,使用领域特有的名词和动词,使得业务逻辑和数据访问紧密结合,提高代码的可读性和可维护性。

领域事件(Domain Event)

在领域模型中发生的一些业务相关的、有意义的事情。这些事件代表了业务流程中的状态转变或重要动作的发生,是领域模型内部状态改变的记录和通知。
领域事件的主要特点和作用包括:

  1. 异步通信:领域事件通常用于在领域模型的不同部分之间,或者在领域模型与外部系统之间进行异步通信。一旦领域内的某个操作触发了事件,系统可以异步地处理这些事件,从而避免了紧耦合和同步等待的问题。
  2. 业务流程驱动:领域事件反映了业务流程中的关键步骤,它们是业务流程的自然产物,有助于捕捉和传播业务流程的进展和变化。
  3. 事件溯源:在事件溯源(Event Sourcing)设计模式中,领域事件更是起到了至关重要的作用。所有的业务操作都被捕获为事件,事件序列成为了系统状态的唯一事实来源,可以用于重建过去的系统状态或追溯业务历史。

事件风暴(Event Storming)

一种快速、直观的团队协作建模技术,用于发掘和理解复杂的业务领域。事件风暴旨在帮助团队成员快速达成对业务流程、领域事件、业务规则和领域模型的共识,通过集体讨论和视觉化的方式快速梳理业务流程,并建立起领域模型的初步构想。
在事件风暴过程中,团队通常会在一块大型画布上工作完成以下步骤:

  1. 事件标记:首先列出业务流程中的关键事件(发生了什么),用便签来表示,贴在画布上。
  2. 命令标识:接着标识出引起这些事件发生的用户行为或系统操作(做了什么),同样以便签形式展示。
  3. 聚合和实体定义:随着事件和命令的浮现,识别出领域模型中的聚合(Aggregates)和实体(Entities),并标明它们在事件流中的作用。
  4. 领域上下文划分:通过讨论,划分出不同的限界上下文(Bounded Context),明确不同上下文之间的交互和依赖关系。
  5. 事件处理逻辑:探讨事件发生后如何处理,即如何响应事件并更新系统状态。
  6. 统一语言提炼:在整个过程中,团队成员不断交流,逐步形成一套符合业务需求的统一语言(Ubiquitous Language),确保所有参与者对业务概念和术语有共同的理解。

3. DDD的分层架构

DDD中的分层架构是一种组织代码结构的方法,旨在提高系统的可维护性、可扩展性和复杂性管理能力。DDD分层架构是针对复杂业务场景下的软件开发策略,它将软件划分为几个具有特定职责的层,每一层都专注于系统内的不同方面。

用户接口层(UI Layer)

  1. 负责与用户的交互,包括但不限于Web界面、桌面应用界面或API接口。
  2. 处理用户请求,格式化显示数据,并将用户操作转换为领域层可以理解的命令或查询。

应用层(Application Layer)

  1. 作为领域层和用户接口层之间的协调层,它不包含任何业务逻辑,而是负责指导领域对象执行任务。
  2. 应用服务在此层实现,负责编排业务流程,调用领域层的服务和模块,并可能处理跨多个领域对象的事务和复杂性。

领域层(Domain Layer)

  1. 是核心业务逻辑所在的地方,包含领域模型(Entities、Value Objects、Aggregates、Domain Services等)。
  2. 定义业务规则、验证业务操作的有效性,并封装了领域内的复杂性。
  3. 领域层还包含了Repository接口,用来管理和检索领域对象,但它不涉及具体的持久化技术细节。

基础设施层(Infrastructure Layer)

  1. 提供底层的技术实现,例如数据存储、消息队列、网络通信等。
  2. 实现领域层所需的Repository接口和其他基础设施服务,如数据库访问、外部API调用等。

4. DDD的挑战

  1. 领域复杂度的认知与把握: DDD要求深入理解业务领域,对于高度复杂的业务场景,充分挖掘和理解领域知识是一项艰巨的任务。业务领域的复杂性和不断变化性可能导致模型难以全面准确地反映现实。
  2. 统一语言的建立与维护: 在团队中推广和维护一套统一的业务语言并不容易,尤其是在跨职能团队和大型项目中,确保每个人都理解并使用统一语言来设计和讨论问题是DDD的一大挑战。
  3. 领域模型的精细化与抽象: 如何恰当地拆分领域模型,定义出恰当的实体、值对象、聚合和领域服务,并不是一件显而易见的事。过度抽象可能导致模型失去实用性,不够抽象则可能导致模型过于复杂和冗余。
  4. 分层架构与领域边界的划分: 设定正确的限界上下文以及在各层之间设计合适的依赖关系,是另一个难点。尤其是在分布式系统和微服务架构中,如何合理划分服务边界尤为重要。
  5. 团队技能与文化的培养: DDD要求团队成员具备领域专业知识、良好的软件设计能力以及跨团队沟通协作的能力。将DDD理念融入团队文化,确保全员参与并认同领域驱动设计方法,是一个长期而持续的努力过程。
  6. 项目进度与成本的压力: 在实际项目开发中,往往面临着时间紧迫和预算约束的压力。深入研究业务领域并实施DDD可能会增加前期的设计和沟通成本,需要权衡短期效益与长期价值。

5. DDD的一些个人理解和疑问

DDD与微服务

DDD和微服务是当前比较流行的两种应用的架构和设计方法。由于它们经常结合在一起使用,容易给人一种两者是一个东西的错觉,但实际上它们各自解决不同的问题,并在实践中可以很好地相互补充和结合。
DDD:

  • DDD是一种软件设计方法论,重点关注于理解复杂的业务领域,并据此建模来解决复杂的业务问题。
  • DDD强调建立领域模型,通过实体、值对象、聚合、领域服务等概念来表达业务逻辑,并提倡使用统一语言来确保业务人员和技术人员对领域知识有相同的理解。
  • DDD在设计上追求高内聚、低耦合,致力于构建反映业务本质且易于维护和扩展的软件系统。

微服务:

  • 微服务是一种架构风格,它提倡将大型的单体应用拆分成一系列小型、自治的服务,每个服务都围绕着一个特定的业务能力或领域进行构建。
  • 每个微服务都具有自己的数据库、业务逻辑,并且可以独立部署、扩展和维护。
  • 微服务的核心理念包括服务的独立性、可替换性、技术选型多样性以及灵活的团队组织方式。

DDD与微服务的结合:

  • 在微服务架构中,可以将DDD的理念应用于每个微服务的设计,每个微服务可以看作是一个独立的领域,拥有自己的限界上下文和领域模型。
  • 通过DDD,每个微服务能够更好地反映和处理其领域内的复杂业务逻辑,并确保每个服务内部的代码结构和业务规则是清晰且一致的。
  • 微服务架构的分治策略与DDD的领域划分策略相得益彰,有助于组织按业务领域组建团队,每个团队专注于一个或几个相关的微服务,更好地遵循DDD的实践。

DDD易学难精的原因

  1. 业务知识难以账务:DDD的核心是深入理解复杂的业务领域,并据此完成建模,而实际业务领域往往包含丰富的规则、逻辑和各种特殊情况。理解和掌握这些复杂的业务逻辑本身就是一项挑战。
  2. 过于依赖实践经验:DDD是一套方法论,并没有过多的提及落地的细则,比如如何划分限界上下文、如何抽象聚合等,更多的需要在多次迭代和实际项目中磨炼,才能真正领悟其精髓。但是,实际工作中,足够复杂的项目是比较少的。
  3. 团队协作要求高:DDD强调领域专家与开发团队之间的密切合作,构建统一语言并共同维护领域模型。实际操作中,达到真正的“领域驱动”需要团队成员间有高度的沟通和默契。
  4. 概念繁多:DDD的概念复杂、术语繁多,很难融会贯通、合理取舍

DDD最有价值的两个概念

限界上下文和统一语言个人认为是DDD中最有价值的两个概念。
限界上下文:框定了领域的边界,统一语言、领域、领域事件、实体等核心概念,都是基于限界上下文进行的。在微服务架构下,限界上下文也能很好的指导微服务拆分。一般一个限界上下文会作为一个微服务。
统一语言:它将复杂的业务领域知识转化成一套简洁且一致的词汇表和概念模型,能够减少领域专家和开发人员日常沟通中的误解和误差,有利于团队成员达成业务共识。同时也能指导开发人员的编码工作,降低代码复杂度,有利于编码知识沉淀。

DDD落地初期可立竿见影的一些尝试

事件风暴:一般的项目流程中,都会有产研测相关同学做需求对齐的活动,事件风暴与该活动是高度重合的,对现有的项目流程不会产生太大的冲击。复杂需求和涉及新业务领域的需求,通过事件风暴这种先发散后收敛的集体讨论活动,可以更好的对齐需求理解,避免不必要的误解。
统一语言:通过事件风暴的发散与收敛,业务专家和技术同学会对齐一些核心概念,这些概念就是在特定限界上下文下的统一语言,可以沉淀成统一的词汇表,指导后续的沟通和开发工作。
统一代码目录&命名:DDD定义了标准的分层和概念,可以据此固化团队内的代码目录结构和关键对象的命名,减少歧义,降低认知复杂度。

事务应该在哪一层

DDD推荐事务放在应用层,主要原因有两个:

  1. 事务属于编排流程,放在应用层,可以让领域层更纯粹的专注于处理业务
  2. 应用层可以决策使用本地事务还是分布式事务
    关于上述两点,个人有些不同的看法:
  3. 事务并不纯粹的是编排流程,多数情况下,是由业务规则决定的在何时何地开事务
  4. 本地大事务风险,如果在应用层开事务,领域层有高耗时操作(一般都会有),如RPC请求等,会出现大事务问题,这个一般是不推荐的
  5. 聚合内部才需要事务保证一致性,跨聚合一般可以异步,既然这样,那就没有跨聚合的事务存在,这样的话DDD中使用分布式事务像是个伪命题?事务开在领域层就够用了?

持久化的粒度

DDD中聚合是持久化的最小粒度,一个聚合对应一个仓储,整存整取。但是这种成本是比较高的。在一些特定场景,比如金融场景,这种聚合粒度的整存整取可能存在一些安全性的问题。比如,聚合中存在金额、币种等与资金相关的信息,每次更细时都save一次,会额外增加意外变更的风险。另外,聚合维度的整存整取,也会给追溯特定字段的变更历史带来一定的困难。

事件溯源

事件溯源将应用程序状态的变化看作一系列不可变的事实记录,通过重放所有的记录,在另外的存储介质中重建数据库记录。这种方式,对于审计追踪和保证数据一致性就比较突出的优势,但是个人感觉,事件溯源在日常工作中,落地会比较少,主要基于以下几个原因:

  1. 大部分业务没有强审计的需求,其他的诸如数据一致性、CQRS等都有其他的手段可以保障,收益不够高
  2. 技术复杂度高,需要处理事件丢失、事件延迟、事件乱序等复杂问题
  3. 存储成本高,需要独立存储所有的事件记录
  4. 错误处理成本高,如果事件记录中存在错误,无法直接更新,可能需要撤销并重新发出事件,或者引入补偿事件等机制,这在实现上较为复杂
  5. 版本兼容和迁移困难,当领域模型发生变化时,需要编写专门的事件处理器,以确保能够同时兼容新老事件

层间依赖

DDD层间依赖的理想结构是:用户接口层 → 应用层 → 领域层 → 基础设施层。这种依赖关系有利于保持业务逻辑的纯粹性,同时降低了各层之间的耦合度,便于后期维护和扩展。
但是在实际落地的过程中,针对一些没有过重业务逻辑的流程(如查询等),经常会出现应用层甚至用户接口层绕过领域层,跨层访问基础设施层的情况。这种跨层访问,虽然一定程度上破坏了DDD的理想层间依赖,但是仍然保留了其核心的单向依赖关系,可以认为是实际落地时的一种取舍。
另一个非理想的层间依赖是,领域层不依赖基础设施层,而是基础设施层依赖领域层(更确切的说,是依赖领域层定义的接口,通过接口实现依赖导致)。原因在于,基础设施相对业务规则是更不稳定的,如使用何种缓存、下游的接口变更等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值