字数 21187,阅读大约需 106 分钟
第二章 领域驱动设计基础
2.1 DDD 核心概念
领域驱动设计(Domain-Driven Design, DDD)是一种软件开发方法论,它将软件的构建与核心业务领域紧密结合。DDD 的核心思想是,在开发复杂软件系统时,应该将领域模型作为核心,并与领域专家进行深入沟通,确保软件模型能够准确反映业务领域的复杂性。对于微服务架构而言,DDD 尤其重要,因为它能帮助我们更好地划分服务边界,构建高内聚、低耦合的微服务。
2.1.1 什么是领域驱动设计
领域驱动设计(DDD)是由Eric Evans在其2003年出版的著作《领域驱动设计:软件核心复杂性应对之道》(Domain-Driven Design: Tackling Complexity in the Heart of Software)中提出的。它不是一种技术,而是一套方法论、一系列原则和模式,旨在帮助开发团队在面对复杂业务领域时,更好地理解业务、建模业务,并将业务逻辑清晰地体现在软件设计中。
DDD 的核心关注点:
- • 领域(Domain): 指的是软件所要解决的特定业务领域,例如电商、金融、医疗等。DDD 强调深入理解这个领域及其业务规则。
- • 领域模型(Domain Model): 它是对领域概念、业务规则和行为的抽象表示,不仅包含数据结构,还融合了业务逻辑和行为。作为软件的核心,领域模型应当准确反映领域专家的心智模型。
- • 通用语言(Ubiquitous Language): 这是一种无歧义的语言,由领域专家和开发人员共同使用,并贯穿于需求分析、设计、编码和测试的整个过程,以确保双方对业务概念的理解一致。。
- • 限界上下文(Bounded Context): 是领域模型存在的明确边界。在不同的限界上下文内,同一个术语可能具有不同的含义,或者同一个概念可能被建模成不同的对象。限界上下文有助于管理复杂性,避免模型混淆。
DDD 的目标:
-
- 深入理解业务: 通过与领域专家的紧密协作,确保开发团队对业务领域有深刻的理解。
-
- 构建高质量的领域模型: 创建一个能够准确反映业务逻辑和行为的软件模型,使其成为软件的核心。
-
- 应对复杂性: 提供一套方法来管理复杂业务领域的软件开发,使其更具可维护性和可扩展性。
-
- 促进沟通: 通过通用语言和限界上下文,改善领域专家和开发人员之间的沟通效率。
2.1.2 DDD 的核心价值与理念
DDD 的核心价值在于它提供了一种应对软件核心复杂性的有效途径,并强调业务价值的实现。其主要理念包括:
-
- 以领域为核心: 软件设计和开发应始终围绕核心业务领域展开,而不是以技术或数据为中心。这意味着业务逻辑是第一位的,技术是为业务服务的。
-
- 持续探索与精炼: 领域模型不是一次性完成的,而是随着对业务理解的深入而持续演进和精炼的。这需要开发人员和领域专家之间的持续沟通和反馈。
-
- 通用语言的重要性: 强调建立和维护一套领域专家和开发人员都能理解和使用的通用语言。这套语言不仅用于沟通,也直接体现在代码中,从而减少歧义和误解。
-
- 显式建模: 领域模型应该显式地存在于代码中,而不是隐藏在数据库脚本或UI逻辑之后。这意味着领域对象应该包含行为,而不仅仅是数据。
-
- 战略设计与战术设计: DDD 将设计过程分为战略设计(关注宏观的领域划分和上下文关系)和战术设计(关注微观的领域模型构建,如实体、值对象、聚合等),提供了一套全面的设计方法。
-
- 关注核心域: 识别并投入更多精力在核心业务领域(Core Domain)的建模上,因为它们是业务竞争力的关键。对于通用域和支撑域,可以采用更简单的解决方案或使用现成的组件。
2.1.3 DDD 与传统开发方法的区别
DDD 与许多传统开发方法在关注点和方法论上存在显著差异。理解这些差异有助于我们更好地认识 DDD 的独特价值。
特性 | 传统开发方法(如数据驱动、功能驱动) | 领域驱动设计(DDD) |
---|---|---|
核心关注点 | 数据结构、数据库设计、功能实现、技术框架 | 核心业务领域、领域模型、业务逻辑、领域专家知识 |
建模方式 | 通常以数据模型(ER图)或功能流程图为中心 | 以领域模型为中心,强调业务概念、行为和关系 |
语言 | 开发人员和业务人员可能使用不同的术语,容易产生误解 | 强调通用语言,确保领域专家和开发人员对业务概念理解一致 |
职责划分 | 业务逻辑可能分散在UI层、数据访问层或服务层,职责不清晰 | 业务逻辑集中在领域层,职责明确,高内聚 |
复杂性管理 | 随着系统规模扩大,复杂性难以控制,容易出现"贫血模型" | 通过限界上下文、聚合等模式管理复杂性,避免模型混淆 |
演进性 | 业务需求变化时,可能需要大规模重构,难以适应 | 领域模型可演进,通过持续重构和精炼适应业务变化 |
团队协作 | 业务人员和开发人员沟通可能存在障碍,需求理解不一致 | 强调领域专家和开发人员的紧密协作,共同构建通用语言和领域模型 |
技术选择 | 技术先行,可能为了使用某种技术而扭曲业务逻辑 | 业务先行,技术选择服务于业务需求,可异构 |
2.1.4 DDD 在微服务中的作用
DDD 和微服务架构是天作之合。微服务强调将大型应用拆分为小型、独立的业务服务,而 DDD 则提供了一套强大的工具和方法来帮助我们识别这些业务边界,并构建出高质量的服务内部模型。
-
- 指导服务边界划分: DDD 中的限界上下文(Bounded Context) 是划分微服务边界的天然依据。每个限界上下文可以自然地映射为一个独立的微服务。这确保了微服务是围绕业务能力而非技术能力进行拆分的,从而实现高内聚、低耦合。
-
- 构建高内聚的领域模型: DDD 的战术设计模式(如实体、值对象、聚合、领域服务、领域事件)指导我们如何在微服务内部构建富领域模型,将业务逻辑和行为封装在领域对象中,避免"贫血模型"。
-
- 促进团队自治: 每个微服务团队可以负责一个或多个限界上下文,并独立地进行领域建模和开发,这与微服务提倡的团队自治理念相符。
-
- 统一语言: 在每个限界上下文内部建立的通用语言,确保了该微服务内部所有成员对业务概念的理解一致,并直接体现在代码中,减少了沟通成本和歧义。
-
- 处理复杂业务逻辑: 对于核心业务领域,DDD 提供了应对复杂业务逻辑的有效方法,使得微服务能够更好地承载和演进这些复杂性。
-
- 事件驱动架构的基础: DDD 中的领域事件(Domain Event) 是构建事件驱动微服务架构的基石。通过发布和订阅领域事件,可以实现微服务间的异步通信和最终一致性,这对于解决分布式事务和数据同步问题至关重要。
-
- 提升可维护性: 清晰的领域模型和明确的服务边界使得微服务更容易理解、测试和维护。
简而言之,DDD 为微服务提供了"灵魂",帮助我们从业务角度思考如何拆分系统,并确保每个拆分出来的服务都具有清晰的业务职责和高质量的内部实现。
2.1.5 DDD 的适用场景与限制
DDD 并非银弹,它有其最能发挥价值的场景,也有其局限性。在决定是否采用 DDD 时,需要进行权衡。
适用场景:
- • 复杂业务领域: 当业务逻辑复杂、规则多变、领域概念抽象时,DDD 能够帮助团队更好地理解和建模业务,避免陷入技术细节而忽略业务本质。
- • 核心业务系统: 对于那些直接关系到企业核心竞争力、需要持续演进和优化的系统,DDD 能够确保软件与业务的紧密对齐。
- • 需要长期维护的系统: DDD 强调构建高质量的领域模型,这有助于提高系统的可维护性和可扩展性,适合需要长期生命周期的项目。
- • 团队具备学习意愿和能力: DDD 引入了一些新的概念和思维方式,需要团队成员具备学习和实践的意愿和能力。
- • 与微服务架构结合: DDD 是划分微服务边界和设计微服务内部结构的强大工具。
限制与不适用场景:
- • 简单CRUD应用: 对于业务逻辑简单、主要涉及数据存储和检索(CRUD)的应用,引入 DDD 可能会增加不必要的复杂性和开销,过度设计。
- • 技术驱动型项目: 如果项目主要关注技术实现而非业务逻辑,或者业务领域非常稳定且不复杂,DDD 的价值可能不明显。
- • 短期项目: DDD 的前期投入(领域探索、通用语言建立、模型精炼)相对较大,对于生命周期很短的项目可能不划算。
- • 团队经验不足: 如果团队对 DDD 概念不熟悉,或者缺乏与领域专家有效沟通的能力,贸然引入 DDD 可能导致项目失败。
- • 领域专家缺失或不配合: DDD 严重依赖领域专家的知识和参与。如果无法获得领域专家的支持,DDD 将难以实施。
总而言之,DDD 是一种强大的工具,但它需要正确的场景和合适的团队来发挥其最大价值。在选择是否采用 DDD 时,应充分评估项目的复杂性、团队能力和业务需求。
2.2 战略设计
领域驱动设计(DDD)的战略设计关注于宏观层面,旨在帮助我们理解和划分大型、复杂的业务领域。它涉及识别核心业务、定义领域边界以及理解不同领域之间的关系。战略设计是微服务架构中服务边界划分的关键指导。
2.2.1 领域(Domain)的识别
在 DDD 中,领域(Domain) 是指软件系统所要解决的特定业务范围或知识领域。它是我们构建软件的基础,包含了业务规则、业务流程和业务概念。识别领域是战略设计的第一步,也是最重要的一步,因为它决定了我们对业务的理解深度和广度。
核心思想:
- • 业务驱动: 领域识别必须从业务需求出发,而不是从技术或数据结构出发。我们需要深入了解业务的本质、目标和痛点。
- • 领域专家: 领域专家(Domain Expert)是领域知识的权威。他们可能是业务分析师、产品经理、销售人员、客服人员,甚至是最终用户。与领域专家进行有效的沟通和协作是识别领域的关键。
- • 共同语言: 在识别领域过程中,需要与领域专家共同建立和使用通用语言(Ubiquitous Language)。通用语言是领域专家和开发人员之间沟通的桥梁,它应该准确、无歧义地描述领域概念和业务规则。
如何识别领域:
-
- 业务访谈与会议: 与领域专家进行深入的访谈和会议,了解他们的日常工作、业务流程、面临的挑战以及期望的解决方案。这通常是获取领域知识最直接有效的方式。
-
- 文档分析: 查阅现有的业务文档、流程图、用户手册、市场分析报告等,从中提取关键的业务概念和规则。
-
- 事件风暴(Event Storming): 这是一种非常有效的协作式建模技术,通过识别业务领域中发生的"事件"来揭示业务流程和领域概念。参与者(包括领域专家和开发人员)通过贴纸在墙上共同绘制业务事件,然后围绕事件识别命令、聚合、读模型等。事件风暴能够快速地帮助团队建立对领域的共同理解。
-
- 用例分析与用户故事: 从用户角度出发,分析用户如何与系统交互,识别核心用例和用户故事。这有助于我们理解系统的功能需求以及这些功能背后的业务逻辑。
-
- 业务流程梳理: 绘制业务流程图,理解业务操作的顺序、决策点和参与者。这有助于揭示领域中的行为和状态转换。
识别领域的输出:
- • 通用语言词汇表: 包含领域中所有关键术语及其精确定义,确保团队成员对这些术语的理解一致。
- • 高层领域模型: 对领域中核心概念、它们之间的关系以及关键业务规则的初步抽象。这可能以概念图、UML类图(高层)、或者简单的文本描述形式存在。
- • 业务流程图: 描述核心业务流程的步骤和逻辑。
通过以上方法,我们可以逐步构建起对业务领域的清晰认识,为后续的子域划分和限界上下文定义奠定基础。
2.2.2 子域(Subdomain)的划分
一个大型业务领域往往过于复杂,难以作为一个整体进行建模和管理。因此,在识别了整个领域之后,下一步就是将其划分为更小、更易于管理的子域(Subdomain)。子域是领域中具有特定业务功能或关注点的逻辑分区。
核心思想:
- • 业务功能: 子域的划分应该基于业务功能或业务能力,而不是技术功能。例如,在一个电商领域中,可以划分为"商品管理"、"订单处理"、"用户管理"、"支付"等子域。
- • 内聚性: 每个子域内部的业务概念和规则应该紧密相关,形成高内聚的逻辑单元。
- • 独立性: 不同的子域之间应该尽可能地独立,减少相互之间的依赖。
- • 领域专家: 子域的划分同样需要领域专家的参与和确认,确保划分符合业务的实际情况。
如何划分子域:
-
- 识别核心业务流程: 分析业务流程,找出其中相对独立的、端到端的功能模块。
-
- 识别关键业务概念: 找出在业务中反复出现的核心名词和动词,它们往往代表了重要的业务概念和行为。
-
- 分析业务团队结构: 康威定律(Conway's Law)指出"设计系统的组织,其产生的设计等同于组织间的沟通结构"。因此,组织结构往往能反映出业务的自然划分。如果不同的业务团队负责不同的业务功能,那么这些功能很可能就是独立的子域。
-
- 事件风暴的产物: 在事件风暴过程中,不同的业务流程和事件集群往往会自然地形成不同的子域。
-
- 避免技术划分: 不要根据技术(如数据库表、UI模块)来划分子域,而应始终以业务为中心。
子域的分类:
在 DDD 中,子域通常被分为三类,这有助于我们分配不同的资源和关注度:
- • 核心域(Core Domain):
- • 定义: 构成企业核心竞争力、最能体现业务价值的子域。它是企业赖以生存和发展的关键,通常具有复杂的业务逻辑和频繁的变化。
- • 关注点: 应该投入最优秀的团队、最先进的技术和最多的精力进行建模和优化。这是 DDD 最能发挥价值的地方。
- • 示例: 在电商系统中,订单处理、推荐算法、库存优化可能是核心域。
- • 支撑域(Supporting Subdomain):
- • 定义: 为核心域提供支持,但本身不构成企业核心竞争力的子域。它们的业务逻辑通常是定制化的,但相对不那么复杂。
- • 关注点: 可以采用相对成熟的技术和设计模式,但仍需根据业务需求进行定制开发。
- • 示例: 用户管理、权限管理、通知服务等,这些服务虽然重要,但通常不是电商的核心竞争力。
- • 通用域(Generic Subdomain):
- • 定义: 业务逻辑通用,不具有行业或企业特异性,并且有现成解决方案或第三方服务的子域。它们在多个业务领域中都可能出现。
- • 关注点: 优先考虑使用现成的产品、开源库或第三方服务,避免重复造轮子。不应该投入过多精力进行定制开发。
- • 示例: 身份认证(OAuth2/OpenID Connect)、日志服务、支付网关集成、短信服务等。
通过对子域的划分和分类,我们可以更有效地分配资源,将精力集中在最能创造业务价值的核心域上,同时合理利用现有资源解决支撑域和通用域的问题。
2.2.3 核心域、支撑域、通用域
如上所述,子域的分类是 DDD 战略设计中的一个重要概念,它指导我们如何分配资源和精力,以最大化业务价值。再次强调这三类子域的特点和处理策略:
-
- 核心域(Core Domain)
- • 特征: 业务的核心竞争力所在,是企业区别于竞争对手的关键。业务逻辑复杂,变化频繁,需要深入的领域知识才能理解和实现。直接影响企业的盈利能力和市场地位。
- • 处理策略:
- • 投入最强团队: 组建由经验丰富的领域专家和开发人员组成的团队。
- • 深入领域建模: 采用 DDD 的战术设计模式(实体、值对象、聚合、领域事件等)进行精细化建模。
- • 持续重构与优化: 随着业务理解的深入和需求变化,持续对核心域模型进行重构和优化。
- • 技术创新: 可以尝试引入新技术或新方法来解决核心域中的复杂问题,以获得竞争优势。
- • 示例: 推荐算法、智能风控、核心交易引擎、基因序列分析等。
-
- 支撑域(Supporting Subdomain)
- • 特征: 对核心域提供支持,但本身不直接构成核心竞争力。业务逻辑通常是定制化的,但相对核心域而言,复杂性较低,变化频率也较低。通常是企业内部特有的业务流程或管理功能。
- • 处理策略:
- • 定制开发: 由于其定制性,通常需要自行开发,但可以采用相对成熟和标准化的技术栈。
- • 适度建模: 可以应用 DDD 的部分原则和模式,但无需像核心域那样进行极致的精炼。
- • 关注效率: 在保证质量的前提下,注重开发效率和成本控制。
- • 示例: 内部报表系统、员工管理、简单的权限管理、内部消息通知系统等。
-
- 通用域(Generic Subdomain)
- • 特征: 业务逻辑非常通用,不具有行业或企业特异性。在许多不同的系统中都可能出现,并且通常有成熟的解决方案或第三方产品可供选择。不涉及企业的核心业务逻辑。
- • 处理策略:
- • 购买或使用开源产品: 优先考虑购买商业软件、使用成熟的开源框架或集成第三方服务。
- • 避免重复造轮子: 坚决避免投入资源进行定制开发,除非现有解决方案无法满足特定需求。
- • 最小化集成成本: 关注如何高效地集成这些通用组件或服务,而不是其内部实现。
- • 示例: 身份认证(Auth0, Keycloak)、日志收集(ELK Stack)、支付网关(Stripe, PayPal)、短信服务(Twilio)、文件存储(AWS S3, Azure Blob Storage)等。
为什么进行子域分类?
- • 资源优化: 将有限的优秀人才和资源集中投入到核心域,以创造最大的业务价值。
- • 风险管理: 降低在非核心业务上投入过多资源而带来的风险。
- • 技术选型: 为不同类型的子域选择最合适的技术和开发策略。
- • 架构演进: 核心域的架构可能需要更灵活、更具演进性的设计,而通用域则可以采用更稳定的、标准化的方案。
通过对子域的清晰分类和差异化处理,企业可以在复杂的业务环境中更有效地进行软件开发和资源管理。
2.2.4 限界上下文(Bounded Context)
限界上下文(Bounded Context) 是 DDD 中最重要的战略设计模式之一,它定义了一个明确的边界,在这个边界之内,特定的领域模型和通用语言是有效的。在不同的限界上下文内,同一个术语可能具有不同的含义,或者同一个概念可能被建模成不同的对象。
核心思想:
- • 模型一致性边界: 限界上下文是领域模型保持一致性的边界。在这个边界内,所有团队成员(包括领域专家和开发人员)对领域概念和通用语言的理解是统一的。
- • 独立演进: 每个限界上下文可以独立地进行开发、部署和演进,而不会影响到其他上下文。
- • 业务功能划分: 限界上下文通常与业务功能或子域紧密相关,但它更强调的是"模型"的边界,而不仅仅是业务功能的划分。
- • 微服务边界: 在微服务架构中,一个限界上下文通常对应一个或一组紧密相关的微服务。它是微服务拆分的最佳依据。
为什么需要限界上下文?
在一个大型复杂的系统中,试图用一个统一的、大一统的领域模型来描述所有业务概念是极其困难且不切实际的。例如,在电商系统中,"商品"这个概念在"商品管理"上下文(关注商品的属性、分类、库存)和"订单管理"上下文(关注订单中的商品快照、价格)中,其关注点和行为是不同的。如果强行使用一个模型,会导致模型臃肿、职责不清,最终难以维护。
限界上下文允许我们在不同的业务场景下,对同一个现实世界概念进行不同的建模,从而简化每个模型的复杂性,并使其更专注于特定的业务问题。
如何识别限界上下文:
-
- 通用语言的边界: 当发现同一个词语在不同团队或不同业务场景下有不同的含义时,这通常是限界上下文的信号。例如,"客户"在销售部门和财务部门的定义可能不同。
-
- 团队组织结构: 不同的业务团队通常负责不同的业务领域,这些团队的边界往往就是限界上下文的边界。
-
- 业务流程的断裂点: 业务流程中出现明显的交接点或转换点,可能预示着不同的限界上下文。
-
- 事件风暴的产物: 事件风暴中,事件的集群和命令的来源往往能帮助我们识别限界上下文。
-
- 避免共享数据库: 如果两个业务功能需要共享同一个数据库,但它们对数据的理解和操作方式不同,那么它们可能属于不同的限界上下文,应该考虑数据独立性。
限界上下文的内部与外部:
- • 内部: 在限界上下文内部,通用语言和领域模型是统一且一致的。所有开发人员和领域专家都使用相同的术语和概念进行沟通和编码。
- • 外部: 当不同的限界上下文需要交互时,它们之间必须通过明确定义的接口(API)进行通信,并且需要进行概念上的转换。例如,一个上下文中的"商品ID"在另一个上下文可能被映射为"产品编号"。
限界上下文与微服务的关系:
- • 一对一: 最理想的情况是一个限界上下文对应一个微服务。这使得微服务能够完全自治,并且其内部模型与业务边界高度一致。
- • 一对多: 一个限界上下文可能包含多个紧密相关的微服务。例如,一个大型的"订单管理"上下文可能包含"订单创建服务"、"订单查询服务"、"订单状态服务"等。
- • 多对一: 极少数情况下,多个限界上下文可能由一个微服务实现,但这通常是反模式,会增加微服务的复杂性。
限界上下文是微服务架构中进行服务拆分和边界定义的核心概念。它帮助我们构建出高内聚、低耦合、可独立演进的微服务系统。
2.2.5 上下文映射(Context Mapping)
当系统被划分为多个限界上下文后,这些上下文之间不可避免地需要进行交互。上下文映射(Context Mapping) 是 DDD 战略设计中的另一个重要模式,它描述了不同限界上下文之间的关系以及它们如何进行集成。
核心思想:
- • 明确关系: 显式地定义不同限界上下文之间的集成模式和依赖关系。
- • 沟通协议: 确定上下文之间如何进行通信,以及谁是上游(提供数据/服务)和谁是下游(消费数据/服务)。
- • 管理复杂性: 通过可视化和文档化上下文之间的关系,帮助团队理解整个系统的宏观结构,并管理集成复杂性。
常见的上下文映射模式:
-
- 合作关系(Partnership):
- • 描述: 两个团队紧密合作,共同开发和维护两个限界上下文。它们之间有共同的成功和失败,需要频繁沟通和协调。
- • 特点: 双方都愿意投入资源来确保集成顺利,通常会共同维护一个共享的通用语言或集成契约。
- • 适用场景: 两个核心业务上下文之间,或者业务功能紧密耦合的场景。
-
- 共享内核(Shared Kernel):
- • 描述: 两个或多个限界上下文共享一部分领域模型或代码。这部分共享的代码是"内核",所有使用它的团队都必须就其变更达成一致。
- • 特点: 减少了重复代码,但增加了团队间的协调成本。变更共享内核需要谨慎,因为它会影响所有依赖方。
- • 适用场景: 多个上下文之间存在少量高度稳定且通用的概念,且团队之间沟通紧密。
-
- 客户/供应商(Customer/Supplier):
- • 描述: 一个上下文(供应商)为另一个上下文(客户)提供服务或数据。客户是供应商的下游,供应商是客户的上游。
- • 特点: 供应商团队对客户团队的需求负责,并需要考虑向后兼容性。客户团队依赖供应商,但通常没有能力影响供应商的开发计划。
- • 适用场景: 常见的上下游依赖关系,如订单服务依赖用户服务获取用户信息。
-
- 遵循者(Conformist):
- • 描述: 下游上下文完全遵循上游上下文的通用语言和模型,不进行任何转换。下游放弃了对上游模型的控制权。
- • 特点: 简化了集成,因为下游无需进行转换。但下游受制于上游模型的变更,可能需要频繁调整。
- • 适用场景: 当上游模型非常稳定且下游没有特殊需求时,或者下游团队资源有限。
-
- 防腐层(Anti-Corruption Layer, ACL):
- • 描述: 下游上下文在与上游上下文交互时,引入一个转换层(防腐层)。防腐层负责将上游模型的概念转换为下游模型可以理解的概念,反之亦然。
- • 特点: 保护下游的领域模型不受上游模型变化的影响,保持下游模型的纯洁性。增加了集成复杂性,因为需要维护转换逻辑。
- • 适用场景: 当上游是遗留系统、外部系统或模型不匹配时,或者下游希望保持自身模型的独立性。
-
- 开放主机服务(Open Host Service, OHS)与发布语言(Published Language, PL):
- • 描述: 上游上下文提供一个明确定义的、公开的 API 或协议(开放主机服务),并使用一种标准化的、文档化的语言(发布语言)来描述其接口和事件。
- • 特点: 允许大量外部系统或下游上下文以标准方式集成,无需紧密协调。发布语言通常是XML Schema、JSON Schema、Protobuf等。
- • 适用场景: 当一个上下文需要被多个外部系统或不确定数量的下游上下文集成时。
-
- 隔离(Separate Ways):
- • 描述: 两个限界上下文之间没有任何集成。它们各自独立发展,互不影响。
- • 特点: 彻底解耦,但可能导致业务功能重复或数据冗余。
- • 适用场景: 两个上下文之间确实没有业务关联,或者集成成本过高且收益甚微。
上下文映射图:
通常会使用上下文映射图来可视化这些关系。一张清晰的上下文映射图能够帮助团队理解整个系统的宏观架构,识别集成点,并指导团队间的沟通和协作。它也是微服务架构中服务间依赖关系的重要文档。
通过战略设计,我们从宏观层面理解了业务领域,将其划分为更小的子域和限界上下文,并明确了这些上下文之间的关系。这为后续的战术设计(微服务内部的详细建模)和微服务拆分奠定了坚实的基础。
2.3 战术设计
领域驱动设计(DDD)的战术设计关注于微观层面,即如何在限界上下文内部构建具体的领域模型。它提供了一系列模式和构建块,帮助我们将战略设计中识别出的业务概念转化为可执行的代码。战术设计是实现高质量领域模型的关键。
2.3.1 实体(Entity)
在 DDD 中,实体(Entity) 是具有唯一标识符的对象,它的生命周期是连续的,并且其标识符在时间上保持不变。实体的核心在于其"身份"而非其属性值。即使实体的属性发生变化,只要其标识符不变,它仍然是同一个实体。
核心特征:
- • 唯一标识符(Identity): 每个实体都必须有一个唯一的标识符,用于区分它与其他实体。这个标识符在实体的整个生命周期中保持不变。例如,用户ID、订单ID、产品ID。
- • 生命周期: 实体通常具有一个明确的生命周期,从创建到销毁。在这个生命周期中,实体的属性可能会发生变化,但其身份保持不变。
- • 行为(Behavior): 实体不仅仅是数据的容器,它应该包含与自身业务逻辑相关的行为。这些行为是领域规则的体现,并且会改变实体的状态。
- • 可变性: 实体的属性通常是可变的,因为它们代表了实体在不同时间点的状态。
实体与数据模型的区别:
传统的数据驱动设计往往将数据库表直接映射为对象,这些对象通常只包含数据,而缺乏行为,被称为"贫血模型"(Anemic Domain Model)。DDD 中的实体则强调"富领域模型"(Rich Domain Model),即实体既包含数据,也包含行为。
特性 | 贫血模型(Anemic Model) | 富领域模型(Rich Domain Model) |
---|---|---|
数据与行为 | 数据与行为分离,行为通常在服务层实现 | 数据与行为封装在一起,行为是对象的一部分 |
职责 | 主要作为数据载体,缺乏业务逻辑 | 包含业务逻辑和领域规则,是业务行为的执行者 |
可维护性 | 业务逻辑分散,难以追踪和维护 | 业务逻辑集中,易于理解和维护 |
测试 | 业务逻辑在服务层,测试需要更多依赖 | 业务逻辑在领域对象中,单元测试更独立、方便 |
实体设计原则:
- • 识别身份: 确保每个实体都有一个明确的唯一标识符。这个标识符可以是业务ID(如订单号),也可以是技术ID(如GUID)。
- • 封装行为: 将与实体相关的业务行为封装在实体内部,而不是放在外部的服务中。例如,
Order
实体应该有confirm()
、cancel()
等方法,而不是在OrderService
中直接操作Order
的属性。 - • 保持一致性: 实体内部的状态变更应该通过其行为方法来完成,以确保业务规则和不变性得到维护。
- • 避免过度设计: 并非所有对象都需要成为实体。如果一个对象没有唯一的身份,并且其属性值决定了其相等性,那么它可能更适合作为值对象。
C# 实体实现示例:
public class Order
{
public Guid Id { get; private set; } // 唯一标识符
public string OrderNumber { get; private set; }
public DateTime OrderDate { get; private set; }
public decimal TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
public List<OrderItem> OrderItems { get; private set; } // 包含值对象集合
// 私有构造函数,强制通过工厂方法或公共构造函数创建,确保业务规则
private Order() { }
public Order(string orderNumber, DateTime orderDate, List<OrderItem> orderItems)
{
if (string.IsNullOrWhiteSpace(orderNumber))
throw new ArgumentException("Order number cannot be null or empty.");
if (orderItems == null || !orderItems.Any())
throw new ArgumentException("Order must have at least one item.");
Id = Guid.NewGuid();
OrderNumber = orderNumber;
OrderDate = orderDate;
Status = OrderStatus.Pending; // 初始状态
OrderItems = orderItems;
CalculateTotalAmount();
}
// 实体行为:确认订单
public void ConfirmOrder()
{
if (Status != OrderStatus.Pending)
throw new InvalidOperationException("Only pending orders can be confirmed.");
Status = OrderStatus.Confirmed;
// 可以在这里发布领域事件,例如 OrderConfirmedEvent
}
// 实体行为:取消订单
public void CancelOrder()
{
if (Status == OrderStatus.Confirmed || Status == OrderStatus.Shipped)
throw new InvalidOperationException("Confirmed or shipped orders cannot be cancelled.");
Status = OrderStatus.Cancelled;
// 可以在这里发布领域事件,例如 OrderCancelledEvent
}
// 内部方法,封装业务逻辑
private void CalculateTotalAmount()
{
TotalAmount = OrderItems.Sum(item => item.Quantity * item.UnitPrice);
}
// 枚举表示订单状态
public enum OrderStatus
{
Pending,
Confirmed,
Shipped,
Cancelled
}
}
public class OrderItem // 这是一个值对象,没有独立的Id,其相等性由属性值决定
{
public string ProductName { get; private set; }
public int Quantity { get; private set; }
public decimal UnitPrice { get; private set; }
public OrderItem(string productName, int quantity, decimal unitPrice)
{
if (string.IsNullOrWhiteSpace(productName))
throw new ArgumentException("Product name cannot be null or empty.");
if (quantity <= 0)
throw new ArgumentException("Quantity must be greater than zero.");
if (unitPrice <= 0)
throw new ArgumentException("Unit price must be greater than zero.");
ProductName = productName;
Quantity = quantity;
UnitPrice = unitPrice;
}
}
在上述示例中,Order
是一个实体,它有唯一的 Id
,并且包含了 ConfirmOrder()
和 CancelOrder()
等业务行为。OrderItem
则是一个值对象,其相等性由 ProductName
、Quantity
和 UnitPrice
决定,它没有独立的身份。
2.3.2 值对象(Value Object)
在 DDD 中,值对象(Value Object) 是一个没有唯一标识符的对象,它的相等性由其所有属性的值来决定。值对象通常用于描述某个概念的属性,并且是不可变的(Immutable)。
核心特征:
- • 无唯一标识符: 值对象没有自己的身份,它只是一个描述性的概念。例如,一个
Address
对象,如果两个Address
对象的街道、城市、邮编都相同,那么它们就是同一个Address
。 - • 相等性基于值: 两个值对象,如果它们的类型相同且所有属性的值都相等,则它们被认为是相等的。
- • 不可变性(Immutability): 值对象一旦创建,其属性值就不能被修改。如果需要改变值对象,应该创建一个新的值对象实例来替代旧的。
- • 行为: 值对象也可以包含行为,但这些行为通常是基于其属性值的计算或转换,并且不会改变值对象自身的状态。
- • 可替换性: 由于值对象是不可变的且相等性基于值,它们可以被完全替换而不会影响引用它们的实体。
值对象与实体的区别:
特性 | 实体(Entity) | 值对象(Value Object) |
---|---|---|
身份 | 具有唯一的标识符,身份在生命周期中保持不变 | 无唯一标识符,身份由其属性值决定 |
相等性 | 基于标识符 | 基于所有属性的值 |
可变性 | 通常是可变的,其属性可以随时间变化 | 通常是不可变的,一旦创建,属性值不能改变 |
生命周期 | 具有独立的生命周期,从创建到销毁 | 没有独立的生命周期,通常依附于实体,随实体创建或销毁 |
用途 | 建模具有生命周期和唯一身份的业务概念 | 建模描述性的概念,通常作为实体的属性 |
示例 | User , Order , Product |
Address , Money , Color , DateRange |
值对象设计原则:
- • 不可变性: 确保值对象的所有属性都是只读的,或者通过私有set访问器来限制外部修改。如果需要修改,则返回一个新的实例。
- • 重写相等性方法: 重写
Equals()
和GetHashCode()
方法,以实现基于值的相等性比较。 - • 封装: 将相关的属性组合成一个有意义的整体,并封装相关的行为。
- • 自验证: 在构造函数中进行必要的验证,确保值对象始终处于有效状态。
C# 值对象实现模式:
在 C# 中实现值对象,通常会创建一个抽象基类 ValueObject
,其中包含 Equals
和 GetHashCode
的通用实现,并要求子类实现 GetEqualityComponents()
方法来返回所有参与相等性比较的属性。
// ValueObject 基类 (通常放在共享的基础设施层或领域层)
public abstract class ValueObject
{
protected static bool EqualOperator(ValueObject left, ValueObject right)
{
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{
return false;
}
return ReferenceEquals(left, null) || left.Equals(right);
}
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
return !(EqualOperator(left, right));
}
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
// 运算符重载,方便比较
public static bool operator ==(ValueObject one, ValueObject two)
{
return EqualOperator(one, two);
}
public static bool operator !=(ValueObject one, ValueObject two)
{
return NotEqualOperator(one, two);
}
}
// 具体的 Money 值对象实现
public class Money : ValueObject
{
public decimal Amount { get; private set; }
public string Currency { get; private set; }
public Money(decimal amount, string currency)
{
if (amount < 0)
throw new ArgumentException("Amount cannot be negative.");
if (string.IsNullOrWhiteSpace(currency))
throw new ArgumentException("Currency cannot be null or empty.");
Amount = amount;
Currency = currency;
}
// 行为:加法
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Cannot add money of different currencies.");
return new Money(Amount + other.Amount, Currency);
}
// 行为:减法
public Money Subtract(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Cannot subtract money of different currencies.");
return new Money(Amount - other.Amount, Currency);
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
public override string ToString()
{
return $"{Amount} {Currency}";
}
}
// 具体的 Address 值对象实现
public class Address : ValueObject
{
public string Street { get; private set; }
public string City { get; private set; }
public string State { get; private set; }
public string ZipCode { get; private set; }
public Address(string street, string city, string state, string zipCode)
{
if (string.IsNullOrWhiteSpace(street) || string.IsNullOrWhiteSpace(city) ||
string.IsNullOrWhiteSpace(state) || string.IsNullOrWhiteSpace(zipCode))
throw new ArgumentException("Address components cannot be null or empty.");
Street = street;
City = city;
State = state;
ZipCode = zipCode;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return State;
yield return ZipCode;
}
public override string ToString()
{
return $"{Street}, {City}, {State} {ZipCode}";
}
}
使用值对象可以使领域模型更加清晰、表达力更强,并且由于其不可变性,可以减少副作用和并发问题,提高代码的健壮性。
2.3.3 聚合(Aggregate)
在 DDD 中,聚合(Aggregate) 是一个或多个实体和值对象的集合,它们被视为一个单一的、内聚的单元,用于数据修改。聚合的目的是为了维护业务不变性(Invariants)和数据一致性。每个聚合都有一个聚合根(Aggregate Root),它是聚合的唯一入口点,所有对聚合内部对象的访问和修改都必须通过聚合根进行。
核心思想:
- • 一致性边界: 聚合定义了一个事务一致性边界。在任何一个事务中,只能修改一个聚合实例。这意味着对聚合内部的所有修改都应该在一个原子操作中完成,以确保聚合内部的数据始终处于有效和一致的状态。
- • 封装: 聚合将内部的实体和值对象封装起来,外部只能通过聚合根来访问和操作它们。这保护了聚合内部的不变性。
- • 聚合根: 聚合根是聚合的唯一对外接口。它负责协调聚合内部所有对象的行为,并确保聚合的不变性规则得到遵守。
聚合的组成:
- • 聚合根(Aggregate Root): 聚合中的一个特定实体,它是聚合的入口点。聚合根负责维护聚合的整体一致性,并对外暴露操作接口。
- • 内部实体和值对象: 聚合内部可以包含其他实体和值对象。这些内部对象不应该被外部直接引用,它们的生命周期由聚合根管理。
聚合设计原则:
-
- 小聚合: 聚合应该尽可能小。大型聚合会增加并发冲突的可能性,降低系统性能。理想情况下,一个聚合只包含一个聚合根和少量相关的实体/值对象。
-
- 通过聚合根引用: 外部对象只能通过聚合根来引用聚合内部的对象。不允许直接引用聚合内部的非根实体或值对象。
-
- 聚合内部一致性: 聚合根负责维护聚合内部的所有不变性规则。任何对聚合内部状态的修改都必须通过聚合根的方法进行,以确保这些规则得到遵守。
-
- 跨聚合引用: 如果一个聚合需要引用另一个聚合,它应该只引用另一个聚合的聚合根的标识符(ID),而不是直接引用整个聚合对象。这有助于保持聚合之间的松耦合,并避免分布式事务。
-
- 一个事务只修改一个聚合: 在一个业务事务中,只允许修改一个聚合实例。如果一个业务操作需要修改多个聚合,那么这通常意味着需要引入分布式事务(如 Saga 模式)或重新考虑聚合边界。
-
- 删除聚合: 删除聚合意味着删除聚合根及其内部所有包含的对象。
聚合设计的好处:
- • 维护一致性: 确保聚合内部的数据始终处于有效状态,简化了数据一致性管理。
- • 简化并发: 由于一个事务只修改一个聚合,可以更容易地处理并发更新,减少锁的粒度。
- • 清晰的边界: 聚合提供了清晰的业务边界和封装,使得领域模型更易于理解和维护。
- • 微服务拆分依据: 聚合是微服务内部进行数据和行为封装的重要单元,也是微服务之间进行数据同步和通信的依据。
C# 聚合实现示例:
// 聚合根基类 (可选,但推荐)
public abstract class AggregateRoot<TId> : Entity<TId>
{
// 可以添加领域事件相关的逻辑,例如:
private readonly List<IDomainEvent> _domainEvents = new List<IDomainEvent>();
protected void AddDomainEvent(IDomainEvent eventItem)
{
_domainEvents.Add(eventItem);
}
public IReadOnlyCollection<IDomainEvent> GetDomainEvents() => _domainEvents.AsReadOnly();
public void ClearDomainEvents() => _domainEvents.Clear();
protected AggregateRoot(TId id) : base(id) { }
}
// Order 聚合根
public class Order : AggregateRoot<Guid>
{
public string OrderNumber { get; private set; }
public DateTime OrderDate { get; private set; }
public decimal TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
public List<OrderItem> OrderItems { get; private set; } // OrderItem 是值对象
// 引用 Customer 聚合根的 ID,而不是 Customer 对象本身
public Guid CustomerId { get; private set; }
private Order() : base(Guid.NewGuid()) { }
public Order(Guid customerId, string orderNumber, DateTime orderDate, List<OrderItem> orderItems)
: base(Guid.NewGuid())
{
if (string.IsNullOrWhiteSpace(orderNumber))
throw new ArgumentException("Order number cannot be null or empty.");
if (orderItems == null || !orderItems.Any())
throw new ArgumentException("Order must have at least one item.");
CustomerId = customerId;
OrderNumber = orderNumber;
OrderDate = orderDate;
Status = OrderStatus.Pending;
OrderItems = orderItems;
CalculateTotalAmount();
// 创建订单时发布领域事件
AddDomainEvent(new OrderCreatedEvent(Id, CustomerId, OrderNumber, TotalAmount));
}
public void ConfirmOrder()
{
if (Status != OrderStatus.Pending)
throw new InvalidOperationException("Only pending orders can be confirmed.");
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedEvent(Id));
}
public void CancelOrder()
{
if (Status == OrderStatus.Confirmed || Status == OrderStatus.Shipped)
throw new InvalidOperationException("Confirmed or shipped orders cannot be cancelled.");
Status = OrderStatus.Cancelled;
AddDomainEvent(new OrderCancelledEvent(Id));
}
private void CalculateTotalAmount()
{
TotalAmount = OrderItems.Sum(item => item.Quantity * item.UnitPrice);
}
public enum OrderStatus
{
Pending,
Confirmed,
Shipped,
Cancelled
}
}
// 领域事件示例
public record OrderCreatedEvent(Guid OrderId, Guid CustomerId, string OrderNumber, decimal TotalAmount) : IDomainEvent;
public record OrderConfirmedEvent(Guid OrderId) : IDomainEvent;
public record OrderCancelledEvent(Guid OrderId) : IDomainEvent;
// 假设的 IDomainEvent 接口
public interface IDomainEvent { }
在这个 Order
聚合中,Order
是聚合根,它包含了 OrderItems
(值对象集合)。所有对订单的修改都通过 Order
聚合根的方法进行,例如 ConfirmOrder()
和 CancelOrder()
,这些方法确保了订单状态的正确转换和业务规则的遵守。同时,它通过 CustomerId
引用了 Customer
聚合,而不是直接包含 Customer
对象,从而保持了聚合间的松耦合。
2.3.4 聚合根(Aggregate Root)
聚合根(Aggregate Root) 是聚合中的一个特殊实体,它是聚合的唯一对外接口。所有对聚合内部对象(包括聚合根自身、其他实体和值对象)的访问和修改都必须通过聚合根进行。聚合根是维护聚合内部一致性的关键。
核心职责:
- • 封装不变性: 聚合根负责维护聚合内部的所有业务不变性规则。这些规则是业务领域中必须始终保持为真的条件。例如,一个订单的总金额必须等于所有订单项金额之和。
- • 生命周期管理: 聚合根管理其内部所有对象的生命周期。当聚合根被删除时,其内部的所有对象也应该被删除。
- • 对外接口: 聚合根对外暴露公共方法,这些方法代表了聚合可以执行的业务操作。外部客户端只能通过这些方法与聚合交互。
- • 协调内部对象: 聚合根协调聚合内部其他实体和值对象的行为,以完成复杂的业务操作。
聚合根设计原则:
-
- 唯一入口: 外部只能通过聚合根来获取聚合内部的任何对象。不允许直接从外部引用聚合内部的非根实体或值对象。
-
- 保护不变性: 聚合根的方法应该确保在任何操作之后,聚合内部的所有不变性规则都得到满足。如果某个操作会破坏不变性,则应该阻止该操作或抛出异常。
-
- 引用标识符: 聚合根如果需要引用其他聚合,应该只引用它们的聚合根的标识符(ID),而不是直接引用整个聚合对象。这避免了跨聚合的直接依赖,有助于保持松耦合和独立性。
-
- 职责单一: 聚合根的职责是维护聚合内部的一致性,而不是处理跨聚合的业务逻辑。跨聚合的业务逻辑应该由领域服务或应用服务来协调。
-
- 小而精: 聚合根应该尽可能地小,只包含完成其核心职责所需的属性和行为。避免将不相关的逻辑或数据堆砌在聚合根中。
聚合根的识别:
- • 谁拥有不变性? 哪个对象负责维护一组业务规则的有效性?这个对象很可能是聚合根。
- • 谁是业务操作的入口? 哪个对象是执行某个业务操作时首先与之交互的对象?
- • 谁的生命周期是独立的? 哪个对象在业务上具有独立的生命周期,并且其内部的其他对象都依附于它?
C# 聚合根示例(同2.3.3节中的 Order 聚合根):
public class Order : AggregateRoot<Guid>
{
// ... 属性和构造函数 ...
// 聚合根的方法,封装业务行为并维护不变性
public void ConfirmOrder()
{
if (Status != OrderStatus.Pending)
throw new InvalidOperationException("Only pending orders can be confirmed.");
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedEvent(Id));
}
public void CancelOrder()
{
if (Status == OrderStatus.Confirmed || Status == OrderStatus.Shipped)
throw new InvalidOperationException("Confirmed or shipped orders cannot be cancelled.");
Status = OrderStatus.Cancelled;
AddDomainEvent(new OrderCancelledEvent(Id));
}
// ... 其他内部方法和属性 ...
}
通过聚合根,我们能够有效地封装领域逻辑,保护领域模型的不变性,并为外部提供清晰、受控的访问接口,从而构建出更加健壮和可维护的领域模型。
2.3.5 领域服务(Domain Service)
在 DDD 中,领域服务(Domain Service) 是一个无状态的对象,它封装了不属于任何实体或值对象的业务逻辑。当某个业务操作涉及多个实体或值对象,或者需要协调多个聚合的行为时,这些逻辑通常会放在领域服务中。
核心特征:
- • 无状态: 领域服务不持有任何状态数据。它的方法是纯粹的函数,只依赖于输入参数和它所依赖的领域对象。
- • 封装业务逻辑: 封装那些不适合放在实体或值对象中的业务逻辑。例如,跨聚合的业务操作、复杂的计算或协调逻辑。
- • 领域概念: 领域服务本身也是领域模型的一部分,它的命名应该反映其业务职责,并使用通用语言。
- • 协调者: 领域服务通常作为协调者,调用一个或多个实体或聚合的方法来完成一个业务操作。
何时使用领域服务:
-
- 操作涉及多个聚合: 当一个业务操作需要协调多个聚合的行为时,例如,从一个账户转账到另一个账户,这涉及到两个
Account
聚合的修改。
- 操作涉及多个聚合: 当一个业务操作需要协调多个聚合的行为时,例如,从一个账户转账到另一个账户,这涉及到两个
-
- 操作不属于任何一个实体: 当某个业务逻辑不自然地属于任何一个实体或值对象时。例如,计算两个地理位置之间的距离,这个逻辑不属于
Location
值对象,也不属于User
实体。
- 操作不属于任何一个实体: 当某个业务逻辑不自然地属于任何一个实体或值对象时。例如,计算两个地理位置之间的距离,这个逻辑不属于
-
- 复杂的计算或验证: 当业务逻辑非常复杂,需要多个步骤或依赖外部系统时,可以将其封装在领域服务中。
-
- 领域中的"动词": 领域服务通常代表了领域中的一个"动词"或"过程",例如
TransferService
、PaymentGatewayService
、RecommendationService
。
- 领域中的"动词": 领域服务通常代表了领域中的一个"动词"或"过程",例如
领域服务与应用服务的区别:
这是一个常见的混淆点。领域服务和应用服务都包含业务逻辑,但它们的职责和所处的层次不同。
特性 | 领域服务(Domain Service) | 应用服务(Application Service) |
---|---|---|
职责 | 封装不属于实体或值对象的领域逻辑,协调领域对象行为 | 协调领域层和基础设施层,处理用例,管理事务,不包含业务逻辑 |
位置 | 领域层(Domain Layer) | 应用层(Application Layer) |
状态 | 无状态 | 无状态 |
输入/输出 | 领域对象作为输入/输出,或基本类型 | DTO(数据传输对象)作为输入/输出,或基本类型 |
依赖 | 依赖实体、值对象、其他领域服务、仓储接口 | 依赖领域服务、仓储、基础设施服务、DTO映射 |
事务 | 不直接管理事务,由应用服务管理 | 管理业务用例的事务边界 |
命名 | 反映领域概念,如 TransferService , PricingService |
反映用例,如 OrderApplicationService , ProductManagementService |
C# 领域服务实现示例:
// 假设有 Account 聚合根
public class Account : AggregateRoot<Guid>
{
public decimal Balance { get; private set; }
public Guid OwnerId { get; private set; }
private Account() : base(Guid.NewGuid()) { }
public Account(Guid id, Guid ownerId, decimal initialBalance)
: base(id)
{
OwnerId = ownerId;
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Deposit amount must be positive.");
Balance += amount;
AddDomainEvent(new AccountDepositedEvent(Id, amount, Balance));
}
public void Withdraw(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Withdraw amount must be positive.");
if (Balance < amount) throw new InvalidOperationException("Insufficient funds.");
Balance -= amount;
AddDomainEvent(new AccountWithdrawnEvent(Id, amount, Balance));
}
}
// 假设有 IAccountRepository 接口用于持久化 Account 聚合
public interface IAccountRepository
{
Task<Account> GetByIdAsync(Guid id);
Task SaveAsync(Account account);
}
// TransferDomainService 领域服务
public class TransferDomainService
{
private readonly IAccountRepository _accountRepository;
public TransferDomainService(IAccountRepository accountRepository)
{
_accountRepository = accountRepository;
}
public async Task TransferFunds(Guid fromAccountId, Guid toAccountId, decimal amount)
{
if (amount <= 0) throw new ArgumentException("Transfer amount must be positive.");
var fromAccount = await _accountRepository.GetByIdAsync(fromAccountId);
var toAccount = await _accountRepository.GetByIdAsync(toAccountId);
if (fromAccount == null) throw new ArgumentException($"Source account {fromAccountId} not found.");
if (toAccount == null) throw new ArgumentException($"Destination account {toAccountId} not found.");
// 业务逻辑:从一个账户扣款,给另一个账户加款
fromAccount.Withdraw(amount);
toAccount.Deposit(amount);
// 持久化修改 (通常由应用服务或工作单元管理事务)
await _accountRepository.SaveAsync(fromAccount);
await _accountRepository.SaveAsync(toAccount);
// 发布领域事件,通知转账完成
// AddDomainEvent(new FundsTransferredEvent(fromAccountId, toAccountId, amount));
}
}
在这个例子中,TransferDomainService
协调了两个 Account
聚合的行为。它不持有状态,只负责执行转账这一业务过程。它依赖于 IAccountRepository
来获取和保存 Account
聚合,但它本身不处理持久化的细节。
2.3.6 领域事件(Domain Event)
在 DDD 中,领域事件(Domain Event) 是指在领域中发生的、业务相关的、值得被其他部分关注和响应的事情。它表示领域模型中的一个状态变化或一个业务事实。领域事件是实现领域模型解耦和构建事件驱动架构的关键。
核心特征:
- • 业务相关性: 领域事件必须是业务领域中具有重要意义的事件,而不是技术事件(如数据库行更新)。例如,"订单已创建"、"库存已扣减"、"用户已注册"。
- • 不可变性: 领域事件一旦发生并被记录,就不能被修改。它代表了一个已经发生的事实。
- • 过去时态命名: 领域事件的命名通常使用过去时态,以表明它是一个已经发生的事情,例如
OrderCreated
、ProductShipped
。 - • 包含上下文: 领域事件应该包含足够的上下文信息,以便事件的消费者能够理解事件的含义并做出响应。例如,
OrderCreatedEvent
可能包含OrderId
、CustomerId
、TotalAmount
等信息。 - • 轻量级: 领域事件应该只包含必要的信息,避免携带整个聚合的状态。
何时使用领域事件:
-
- 通知其他聚合或服务: 当一个聚合的某个行为导致了其他聚合或服务需要做出响应时。例如,订单创建后,库存服务需要扣减库存。
-
- 实现最终一致性: 在分布式系统中,通过发布和订阅领域事件来协调多个服务的操作,实现数据之间的最终一致性。
-
- 解耦领域模型: 领域事件使得发布者和订阅者之间松耦合,发布者不需要知道谁会响应它的事件,只需要发布即可。
-
- 审计和追踪: 领域事件可以作为业务操作的完整历史记录,方便审计和回溯。
-
- 与 CQRS 和事件溯源结合: 领域事件是实现 CQRS 读模型更新和事件溯源的基础。
领域事件的生命周期:
-
- 创建: 在聚合根或领域服务中,当某个业务操作完成后,创建相应的领域事件实例。
-
- 收集: 聚合根通常会收集其内部产生的领域事件。
-
- 发布: 在业务事务提交(通常在应用服务或工作单元中)之后,发布这些领域事件。发布可以是同步的(在同一进程内)或异步的(通过消息队列)。
-
- 处理: 领域事件的订阅者(事件处理器)接收到事件后,执行相应的业务逻辑。
集成事件 vs 领域事件:
这是一个重要的区分点。
特性 | 领域事件(Domain Event) | 集成事件(Integration Event) |
---|---|---|
范围 | 单个限界上下文内部 | 跨多个限界上下文或微服务 |
目的 | 协调同一个限界上下文内部的领域逻辑,或作为跨上下文异步通信的触发器 | 协调不同微服务或外部系统之间的业务流程,实现最终一致性 |
发布方式 | 可以是同步内存发布,也可以是异步消息队列发布 | 通常是异步消息队列发布,需要持久化和可靠传输 |
事务 | 与当前业务事务绑定,在事务提交后发布 | 独立于当前业务事务,通常在本地事务提交后,通过消息总线发布 |
处理 | 由领域事件处理器处理,通常在同一进程内 | 由集成事件处理器处理,通常在不同进程或服务中 |
示例 | OrderConfirmedEvent (内部事件) |
OrderConfirmedIntegrationEvent (跨服务事件) |
C# 领域事件实现示例:
// 领域事件接口
public interface IDomainEvent { }
// 具体的领域事件类 (通常是不可变的记录类型)
public record OrderCreatedEvent(Guid OrderId, Guid CustomerId, string OrderNumber, decimal TotalAmount) : IDomainEvent;
public record OrderConfirmedEvent(Guid OrderId) : IDomainEvent;
public record InventoryReducedEvent(Guid ProductId, int Quantity) : IDomainEvent;
// 领域事件发布器接口 (通常在基础设施层实现)
public interface IDomainEventDispatcher
{
Task DispatchEventsAsync(IEnumerable<IDomainEvent> events);
}
// 聚合根中添加领域事件的示例 (同2.3.3节)
public class Order : AggregateRoot<Guid>
{
private readonly List<IDomainEvent> _domainEvents = new List<IDomainEvent>();
protected void AddDomainEvent(IDomainEvent eventItem)
{
_domainEvents.Add(eventItem);
}
public IReadOnlyCollection<IDomainEvent> GetDomainEvents() => _domainEvents.AsReadOnly();
public void ClearDomainEvents() => _domainEvents.Clear();
public void ConfirmOrder()
{
// ... 业务逻辑 ...
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedEvent(Id)); // 产生领域事件
}
}
// 领域事件处理器示例
public class InventoryEventHandler : IHandleDomainEvent<OrderConfirmedEvent>
{
private readonly IProductRepository _productRepository;
public InventoryEventHandler(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task Handle(OrderConfirmedEvent domainEvent)
{
// 根据 OrderConfirmedEvent 中的信息,执行库存扣减逻辑
// 假设这里需要获取订单详情来知道扣减哪些商品和数量
// 实际中,OrderConfirmedEvent 可能需要包含更多商品信息
Console.WriteLine($"Handling OrderConfirmedEvent for OrderId: {domainEvent.OrderId}");
// 模拟库存扣减
// var order = await _orderRepository.GetByIdAsync(domainEvent.OrderId);
// foreach (var item in order.OrderItems)
// {
// var product = await _productRepository.GetByIdAsync(item.ProductId);
// product.ReduceStock(item.Quantity);
// await _productRepository.SaveAsync(product);
// AddDomainEvent(new InventoryReducedEvent(item.ProductId, item.Quantity));
// }
}
}
// 假设的事件处理器接口
public interface IHandleDomainEvent<TEvent> where TEvent : IDomainEvent
{
Task Handle(TEvent domainEvent);
}
领域事件是构建响应式、解耦的领域模型的强大工具,尤其在微服务架构中,它能够有效地协调跨服务的业务流程,并实现最终一致性。
2.4 DDD 分层架构
领域驱动设计(DDD)推荐采用一种清晰的分层架构,以确保领域模型的核心地位,并将其与技术细节分离。这种分层有助于保持领域模型的纯净性,提高代码的可维护性和可测试性。典型的 DDD 分层架构通常包括用户界面层、应用层、领域层和基础设施层。
2.4.1 用户界面层(User Interface Layer)
用户界面层(User Interface Layer) ,也称为 表示层(Presentation Layer),是用户与系统交互的入口。它负责向用户展示信息,并接收用户的输入。这一层不包含任何业务逻辑,其主要职责是将用户请求转换为应用层可以理解的命令,并将应用层返回的数据格式化后展示给用户。
主要职责:
- • 用户交互: 提供用户界面(Web页面、桌面应用、移动应用、API接口等),接收用户输入。
- • 数据展示: 将应用层返回的数据进行格式化和渲染,展示给用户。
- • 请求转换: 将用户操作转换为应用层可以处理的命令(Command)或查询(Query)。
- • 输入验证: 进行基本的输入格式验证,但不涉及业务规则验证。
- • 会话管理: 管理用户会话和认证信息。
特点:
- • 薄层: 这一层应该尽可能地薄,不包含任何业务逻辑。所有业务逻辑都应该委托给应用层或领域层。
- • 依赖应用层: 用户界面层直接依赖于应用层,通过调用应用服务来完成用户请求。
- • 技术多样性: 可以采用各种前端技术(如 React, Angular, Vue.js)或后端API框架(如 ASP.NET Core Web API, Flask)来实现。
C# 示例:
在 ASP.NET Core Web API 中,Controller 就属于用户界面层:
// UserInterfaceLayer/Controllers/OrderController.cs
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderApplicationService _orderApplicationService;
public OrdersController(IOrderApplicationService orderApplicationService)
{
_orderApplicationService = orderApplicationService;
}
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
// 1. 简单的输入格式验证 (业务规则验证应在应用层或领域层)
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// 2. 将请求转换为应用层命令
var command = new CreateOrderCommand(
request.CustomerId,
request.OrderNumber,
request.OrderItems.Select(item => new CreateOrderCommand.OrderItemDto(item.ProductName, item.Quantity, item.UnitPrice)).ToList()
);
// 3. 调用应用服务处理命令
var orderId = await _orderApplicationService.CreateOrder(command);
// 4. 返回结果
return CreatedAtAction(nameof(GetOrderById), new { id = orderId }, null);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetOrderById(Guid id)
{
// 1. 调用应用服务查询数据
var orderDto = await _orderApplicationService.GetOrderDetails(new GetOrderQuery(id));
if (orderDto == null)
{
return NotFound();
}
// 2. 返回结果
return Ok(orderDto);
}
}
// 请求DTO (Data Transfer Object)
public class CreateOrderRequest
{
public Guid CustomerId { get; set; }
public string OrderNumber { get; set; }
public List<OrderItemRequest> OrderItems { get; set; }
public class OrderItemRequest
{
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
}
2.4.2 应用层(Application Layer)
应用层(Application Layer) 是 DDD 分层架构中的协调者。它不包含业务逻辑,而是负责协调领域对象和基础设施对象来完成特定的业务用例。应用层定义了系统的外部接口,并管理业务操作的事务边界。
主要职责:
- • 定义用例: 暴露系统可以执行的所有业务用例(Use Case)。每个用例通常对应一个应用服务方法。
- • 协调领域对象: 调用领域层中的实体、聚合和领域服务来执行业务逻辑。
- • 管理事务: 负责管理业务操作的事务边界,确保数据的一致性。例如,在操作开始时开启事务,操作结束时提交或回滚事务。
- • 数据转换: 将用户界面层传入的 DTO(Data Transfer Object)转换为领域对象,并将领域对象转换为 DTO 返回给用户界面层。
- • 安全与授权: 执行粗粒度的安全检查和授权,例如检查用户是否有权限执行某个操作。
- • 不包含业务逻辑: 应用层本身不包含任何业务规则或领域知识。它只是一个协调器,将请求路由到正确的领域对象或基础设施服务。
特点:
- • 薄层: 应该保持精简,只包含协调逻辑,不包含复杂的业务规则。
- • 依赖领域层和基础设施层: 应用层依赖于领域层来执行业务逻辑,依赖于基础设施层来完成数据持久化、消息发送等操作。
- • 面向用例: 应用服务的方法通常以业务用例命名,例如
CreateOrder
、ProcessPayment
。
C# 示例:
// ApplicationLayer/Services/OrderApplicationService.cs
public class OrderApplicationService : IOrderApplicationService
{
private readonly IOrderRepository _orderRepository;
private readonly IUnitOfWork _unitOfWork; // 用于管理事务
private readonly IDomainEventDispatcher _domainEventDispatcher; // 用于发布领域事件
public OrderApplicationService(
IOrderRepository orderRepository,
IUnitOfWork unitOfWork,
IDomainEventDispatcher domainEventDispatcher)
{
_orderRepository = orderRepository;
_unitOfWork = unitOfWork;
_domainEventDispatcher = domainEventDispatcher;
}
// 用例:创建订单
public async Task<Guid> CreateOrder(CreateOrderCommand command)
{
// 1. 粗粒度验证 (例如,参数非空检查,更细致的业务验证在领域层)
if (command == null) throw new ArgumentNullException(nameof(command));
// ... 更多验证 ...
// 2. 协调领域对象:创建 Order 聚合根
var orderItems = command.OrderItems
.Select(item => new OrderItem(item.ProductName, item.Quantity, item.UnitPrice))
.ToList();
var order = new Order(command.CustomerId, command.OrderNumber, DateTime.UtcNow, orderItems);
// 3. 调用基础设施层:持久化 Order 聚合
await _orderRepository.AddAsync(order);
// 4. 管理事务:提交工作单元
await _unitOfWork.CommitAsync();
// 5. 发布领域事件 (在事务提交后)
await _domainEventDispatcher.DispatchEventsAsync(order.GetDomainEvents());
order.ClearDomainEvents(); // 清除已发布的事件
return order.Id;
}
// 用例:获取订单详情 (查询操作)
public async Task<OrderDto> GetOrderDetails(GetOrderQuery query)
{
var order = await _orderRepository.GetByIdAsync(query.OrderId);
if (order == null) return null;
// 转换为 DTO 返回
return new OrderDto
{
OrderId = order.Id,
OrderNumber = order.OrderNumber,
OrderDate = order.OrderDate,
TotalAmount = order.TotalAmount,
Status = order.Status.ToString(),
OrderItems = order.OrderItems.Select(item => new OrderDto.OrderItemDto
{
ProductName = item.ProductName,
Quantity = item.Quantity,
UnitPrice = item.UnitPrice
}).ToList()
};
}
}
// 命令 (Command) - 改变系统状态的请求
public record CreateOrderCommand(Guid CustomerId, string OrderNumber, List<CreateOrderCommand.OrderItemDto> OrderItems)
{
public record OrderItemDto(string ProductName, int Quantity, decimal UnitPrice);
}
// 查询 (Query) - 获取系统状态的请求
public record GetOrderQuery(Guid OrderId);
// 数据传输对象 (DTO) - 用于在应用层和用户界面层之间传输数据
public class OrderDto
{
public Guid OrderId { get; set; }
public string OrderNumber { get; set; }
public DateTime OrderDate { get; set; }
public decimal TotalAmount { get; set; }
public string Status { get; set; }
public List<OrderItemDto> OrderItems { get; set; }
public class OrderItemDto
{
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
}
// 应用服务接口
public interface IOrderApplicationService
{
Task<Guid> CreateOrder(CreateOrderCommand command);
Task<OrderDto> GetOrderDetails(GetOrderQuery query);
}
2.4.3 领域层(Domain Layer)
领域层(Domain Layer) 是 DDD 分层架构的核心。它包含了业务领域的所有概念、业务规则和业务逻辑。这一层是"业务的心脏",它独立于任何技术实现细节,只关注业务本身。
主要职责:
- • 封装业务逻辑: 包含所有核心业务规则和领域知识。这些逻辑应该封装在实体、值对象和领域服务中。
- • 维护领域不变性: 确保领域模型始终处于有效和一致的状态,通过聚合根来强制执行不变性规则。
- • 表达通用语言: 领域层的代码应该直接反映通用语言,使得领域专家和开发人员能够更容易地理解代码。
- • 发布领域事件: 当领域中发生重要事件时,领域对象会发布领域事件,供其他部分订阅和响应。
组成部分:
- • 实体(Entities): 具有唯一标识符和生命周期的对象,包含行为和数据。例如
Order
,Customer
,Product
。 - • 值对象(Value Objects): 没有唯一标识符,其相等性由属性值决定,通常是不可变的。例如
Address
,Money
,OrderItem
。 - • 聚合(Aggregates): 一个或多个实体和值对象的集合,被视为一个单一的事务一致性单元,由聚合根管理。例如
Order
聚合。 - • 领域服务(Domain Services): 封装不属于任何实体或值对象的业务逻辑,通常用于协调多个领域对象。例如
TransferDomainService
。 - • 领域事件(Domain Events): 表示领域中发生的、业务相关的、值得被其他部分关注和响应的事情。例如
OrderCreatedEvent
。 - • 仓储接口(Repository Interfaces): 定义了持久化领域对象(特别是聚合根)的契约。具体的实现放在基础设施层。
特点:
- • 核心: 领域层是整个应用程序的核心,它包含了最重要的业务价值。
- • 独立性: 领域层不应该依赖于任何其他层(除了它自己内部的组件)。它应该是一个纯粹的业务模型,不包含任何基础设施或用户界面相关的代码。
- • 高内聚: 领域层内部的组件应该紧密相关,共同完成业务目标。
- • 可测试性: 由于不依赖外部技术细节,领域层非常容易进行单元测试。
C# 示例:
// DomainLayer/Entities/Order.cs (聚合根)
public class Order : AggregateRoot<Guid>
{
// ... 属性和构造函数 ...
public void ConfirmOrder()
{
if (Status != OrderStatus.Pending)
throw new InvalidOperationException("Only pending orders can be confirmed.");
Status = OrderStatus.Confirmed;
AddDomainEvent(new OrderConfirmedEvent(Id));
}
// ... 其他业务行为 ...
}
// DomainLayer/ValueObjects/OrderItem.cs
public class OrderItem : ValueObject
{
// ... 属性和构造函数 ...
protected override IEnumerable<object> GetEqualityComponents()
{
yield return ProductName;
yield return Quantity;
yield return UnitPrice;
}
}
// DomainLayer/Services/TransferDomainService.cs
public class TransferDomainService
{
private readonly IAccountRepository _accountRepository;
public TransferDomainService(IAccountRepository accountRepository)
{
_accountRepository = accountRepository;
}
public async Task TransferFunds(Guid fromAccountId, Guid toAccountId, decimal amount)
{
// ... 业务逻辑 ...
}
}
// DomainLayer/Repositories/IOrderRepository.cs (仓储接口)
public interface IOrderRepository
{
Task<Order> GetByIdAsync(Guid id);
Task AddAsync(Order order);
Task UpdateAsync(Order order);
Task DeleteAsync(Guid id);
}
// DomainLayer/Events/OrderCreatedEvent.cs
public record OrderCreatedEvent(Guid OrderId, Guid CustomerId, string OrderNumber, decimal TotalAmount) : IDomainEvent;
2.4.4 基础设施层(Infrastructure Layer)
基础设施层(Infrastructure Layer) 是 DDD 分层架构的最底层。它负责提供所有技术支持,实现领域层和应用层所需的通用技术能力。这一层包含了所有与外部系统、数据库、文件系统、消息队列、网络通信等技术细节相关的代码。
主要职责:
- • 持久化: 实现仓储接口(Repository Interface),负责领域对象的持久化(保存、加载、更新、删除)。例如,使用 Entity Framework Core 实现
IOrderRepository
。 - • 消息传递: 实现消息发送和接收机制,用于领域事件的发布和订阅,或服务间的异步通信。
- • 外部服务集成: 调用外部 API、第三方服务等。
- • 配置管理: 读取和管理应用程序的配置信息。
- • 日志与监控: 实现日志记录、指标收集和分布式追踪等功能。
- • 安全: 实现认证、授权等安全机制。
- • 通用工具: 提供其他通用的技术工具和辅助功能。
特点:
- • 依赖倒置: 基础设施层依赖于领域层和应用层定义的接口(如仓储接口),而不是领域层和应用层依赖于基础设施层的具体实现。这是通过依赖注入(Dependency Injection)实现的。
- • 技术细节: 包含了所有与特定技术相关的代码,如数据库连接字符串、ORM 配置、消息队列客户端等。
- • 可替换性: 由于依赖倒置原则,基础设施层的具体实现可以被替换,而不会影响到领域层和应用层。
C# 示例:
// InfrastructureLayer/Repositories/OrderRepository.cs (IOrderRepository 的实现)
public class OrderRepository : IOrderRepository
{
private readonly ApplicationDbContext _dbContext;
public OrderRepository(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Order> GetByIdAsync(Guid id)
{
return await _dbContext.Orders
.Include(o => o.OrderItems) // 加载值对象集合
.FirstOrDefaultAsync(o => o.Id == id);
}
public async Task AddAsync(Order order)
{
await _dbContext.Orders.AddAsync(order);
}
public Task UpdateAsync(Order order)
{
_dbContext.Orders.Update(order);
return Task.CompletedTask;
}
public async Task DeleteAsync(Guid id)
{
var order = await _dbContext.Orders.FindAsync(id);
if (order != null)
{
_dbContext.Orders.Remove(order);
}
}
}
// InfrastructureLayer/Data/ApplicationDbContext.cs (Entity Framework Core DbContext)
public class ApplicationDbContext : DbContext, IUnitOfWork
{
public DbSet<Order> Orders { get; set; }
// ... 其他 DbSet ...
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置 Order 聚合的映射
modelBuilder.Entity<Order>(b =>
{
b.HasKey(o => o.Id);
b.Property(o => o.OrderNumber).IsRequired().HasMaxLength(50);
b.Property(o => o.TotalAmount).HasColumnType("decimal(18,2)");
b.OwnsMany(o => o.OrderItems, oi => // 配置 OrderItem 作为值对象集合
{
oi.WithOwner().HasForeignKey("OrderId");
oi.Property(item => item.ProductName).IsRequired().HasMaxLength(100);
oi.Property(item => item.UnitPrice).HasColumnType("decimal(18,2)");
oi.ToTable("OrderItems"); // 映射到单独的表
});
b.Property(o => o.Status).HasConversion<string>(); // 枚举转字符串存储
});
base.OnModelCreating(modelBuilder);
}
public async Task<bool> CommitAsync()
{
// 在这里可以发布领域事件
// var domainEvents = ChangeTracker.Entries<AggregateRoot<Guid>>()
// .SelectMany(x => x.Entity.GetDomainEvents())
// .ToList();
var result = await base.SaveChangesAsync() > 0;
// foreach (var domainEvent in domainEvents)
// {
// await _domainEventDispatcher.DispatchAsync(domainEvent);
// }
return result;
}
}
// InfrastructureLayer/Events/DomainEventDispatcher.cs (IDomainEventDispatcher 的实现)
public class DomainEventDispatcher : IDomainEventDispatcher
{
private readonly IServiceProvider _serviceProvider;
public DomainEventDispatcher(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task DispatchEventsAsync(IEnumerable<IDomainEvent> events)
{
foreach (var domainEvent in events)
{
// 通过依赖注入容器获取所有对应的事件处理器
var handlerType = typeof(IHandleDomainEvent<>).MakeGenericType(domainEvent.GetType());
var handlers = _serviceProvider.GetServices(handlerType);
foreach (dynamic handler in handlers)
{
await handler.Handle((dynamic)domainEvent);
}
}
}
}
2.4.5 各层职责与依赖关系
DDD 分层架构的成功在于其清晰的职责划分和严格的依赖关系。理解这些关系对于构建健壮、可维护的系统至关重要。
各层职责总结:
- • 用户界面层(User Interface Layer):
- • 职责: 负责用户交互、数据展示、请求转换和会话管理。
- • 核心: 接收用户输入,将请求传递给应用层,展示应用层返回的结果。
- • 不包含: 任何业务逻辑。
- • 应用层(Application Layer):
- • 职责: 协调领域层和基础设施层,定义和实现业务用例,管理事务边界。
- • 核心: 编排领域对象和基础设施服务来完成特定的业务流程。
- • 不包含: 任何业务逻辑(它只是协调者)。
- • 领域层(Domain Layer):
- • 职责: 封装所有核心业务规则和领域知识,维护领域不变性。
- • 核心: 业务的心脏,包含实体、值对象、聚合、领域服务、领域事件和仓储接口。
- • 不包含: 任何用户界面、应用协调或基础设施技术细节。
- • 基础设施层(Infrastructure Layer):
- • 职责: 提供所有技术支持,实现领域层和应用层所需的通用技术能力。
- • 核心: 实现持久化、消息传递、外部服务集成、配置管理、日志监控等。
- • 不包含: 任何业务逻辑。
依赖关系:
DDD 分层架构严格遵循依赖倒置原则(Dependency Inversion Principle, DIP),即高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
- • 用户界面层 依赖 应用层。
- • 应用层 依赖 领域层 和 基础设施层(通过接口)。
- • 领域层 不依赖任何其他层,它是独立的。它定义了仓储接口,但具体的实现由基础设施层提供。
- • 基础设施层 依赖 领域层 和 应用层 定义的接口,实现具体的持久化、消息传递等功能。
依赖关系图:
定义接口
用户界面层 应用层 领域层 基础设施层
为什么这种分层很重要?
-
- 关注点分离: 每层只关注自己的职责,使得代码更清晰、更易于理解和维护。
-
- 提高可测试性: 领域层不依赖于任何外部技术细节,可以独立进行单元测试,确保业务逻辑的正确性。
-
- 保持领域模型纯净: 领域层不受技术细节的污染,能够更好地反映业务领域,降低了技术债务。
-
- 提高可替换性: 基础设施层的实现可以被替换(例如,从SQL Server切换到MongoDB),而不会影响到上层业务逻辑。
-
- 促进团队协作: 不同的团队可以专注于不同的层次,例如前端团队专注于用户界面层,后端团队专注于应用层和领域层,运维团队专注于基础设施层。
通过这种清晰的分层和严格的依赖关系,DDD 帮助我们构建出高内聚、低耦合、易于演进和维护的复杂业务系统。
2.5 通用语言与模型
在领域驱动设计(DDD)中,通用语言(Ubiquitous Language) 和 领域模型(Domain Model) 是两个相互关联且至关重要的概念。它们是 DDD 的基石,确保了领域专家和开发人员之间能够高效、准确地沟通,并将业务知识无缝地转化为软件设计。
2.5.1 通用语言(Ubiquitous Language)
通用语言(Ubiquitous Language) 是指在特定限界上下文内,领域专家和开发人员共同使用的、无歧义的语言。它不仅仅是术语的集合,更是一种思维方式和沟通工具,贯穿于需求分析、设计、编码、测试和部署的整个软件开发生命周期。
核心思想:
- • 共同理解: 确保团队中所有成员(包括业务人员和技术人员)对业务概念、业务规则和业务流程有统一的理解。
- • 无歧义性: 消除因不同背景和视角造成的术语歧义。同一个词在不同语境下可能含义不同,通用语言则明确了其在当前限界上下文中的唯一含义。
- • 体现在代码中: 通用语言不仅仅用于口头沟通和文档,更重要的是,它应该直接体现在领域模型的代码中。类名、方法名、变量名都应该使用通用语言中的术语。
通用语言的建立与演进:
-
- 领域专家参与: 通用语言的建立离不开领域专家的积极参与。开发人员需要主动向领域专家学习,理解他们的业务术语和心智模型。
-
- 持续沟通与迭代: 通用语言不是一次性定义的,而是随着对领域理解的深入而持续演进和精炼的。在日常的交流、会议、事件风暴等活动中,不断发现、澄清和完善通用语言。
-
- 文档化: 维护一个通用语言词汇表或术语表,记录关键术语的定义和示例,供团队成员参考。
-
- 体现在代码中: 这是最重要的一点。如果代码中使用的术语与通用语言不一致,那么通用语言的价值就会大打折扣。代码是通用语言的最终体现。
通用语言的益处:
- • 提高沟通效率: 减少沟通障碍和误解,加速需求分析和设计过程。
- • 减少返工: 避免因理解偏差导致的设计错误和开发偏差。
- • 提升代码质量: 代码更具可读性、可维护性,因为它们直接反映了业务概念。
- • 促进团队协作: 建立共同的思维框架,增强团队凝聚力。
- • 保持领域模型纯净: 确保领域模型与业务保持一致,避免技术细节的侵蚀。
示例:
在一个电商系统中,关于"商品"的通用语言可能包括:
- • 商品(Product): 指的是在售的、具有唯一标识的物品,包含名称、描述、价格、库存等属性。
- • SKU(Stock Keeping Unit): 商品的最小销售单元,可能包含颜色、尺寸等变体信息。
- • 订单项(OrderItem): 订单中的一个具体商品,包含商品快照信息、购买数量和当时的价格。
- • 库存(Inventory): 某个 SKU 在仓库中的可用数量。
如果开发人员在代码中将"商品"命名为 Item
或 Goods
,或者将"订单项"命名为 OrderLine
,而领域专家习惯使用"商品"和"订单项",那么就产生了语言上的不一致,这会增加沟通成本和理解难度。
2.5.2 领域模型(Domain Model)
领域模型(Domain Model) 是对特定限界上下文内业务概念、业务规则和业务行为的抽象表示。它是通用语言的直接体现,也是软件系统的核心。领域模型不仅仅是数据结构,它包含了丰富的行为和业务逻辑。
核心思想:
- • 业务核心: 领域模型是软件的核心,它应该能够准确地反映业务领域的复杂性,并解决业务问题。
- • 行为与数据封装: 领域模型中的对象(实体、值对象、聚合)应该封装数据和行为,而不是"贫血模型"。
- • 通用语言的体现: 领域模型的结构、命名和行为都应该直接来源于通用语言。
- • 独立于技术: 领域模型应该独立于任何技术实现细节(如数据库、UI框架),只关注业务逻辑。
领域模型的构建与演进:
-
- 从通用语言开始: 领域模型的构建始于通用语言。通过通用语言识别出核心概念、它们之间的关系以及业务规则。
-
- 识别实体、值对象和聚合: 根据业务概念的特性(是否有唯一身份、是否可变、是否需要维护一致性边界)来识别实体、值对象和聚合。
-
- 封装行为: 将与领域对象相关的业务行为封装在对象内部,而不是放在外部的服务中。
-
- 维护不变性: 通过聚合根来维护聚合内部的不变性规则,确保数据的一致性。
-
- 持续重构与精炼: 领域模型不是一蹴而就的,而是随着对业务理解的深入和业务需求的变化而持续重构和精炼的。这需要开发人员和领域专家之间的持续反馈。
-
- 事件风暴: 事件风暴是一种非常有效的工具,可以帮助团队共同发现领域事件、命令、聚合和读模型,从而构建出更准确的领域模型。
领域模型的表现形式:
领域模型可以有多种表现形式,但最终都会体现在代码中:
- • 概念模型: 高层次的业务概念图,用于帮助团队理解业务全貌。
- • UML 类图: 详细的类图,展示实体、值对象、聚合之间的关系和属性。
- • 代码: 最终的领域模型以代码的形式存在,包括类、接口、枚举等。
领域模型与微服务的关系:
在微服务架构中,每个微服务都应该拥有一个清晰的领域模型,这个模型对应于该微服务所负责的限界上下文。一个好的领域模型能够指导微服务的内部设计,使其高内聚、低耦合,并能够独立演进。
- • 限界上下文是领域模型的边界: 每个限界上下文都有其独立的领域模型和通用语言。
- • 领域模型指导微服务内部实现: 领域模型中的实体、值对象、聚合、领域服务等构建块,直接构成了微服务内部的业务逻辑。
- • 领域事件促进微服务间通信: 领域模型中产生的领域事件,可以作为微服务之间异步通信的触发器,实现服务间的解耦。