原文链接:https://blog.bytebytego.com/p/a-crash-course-on-domain-driven-design

为复杂领域开发软件是一项具有挑战性的任务。

随着问题领域的复杂性不断增长,创建准确表示业务概念、规则和流程的软件变得越来越困难。设计不良的软件很快就会变成难以理解、难以维护和扩展的混乱代码。

领域驱动设计(DDD)为这个问题提供了解决方案。

DDD 是一种软件开发方法,它通过强调对核心领域和业务逻辑进行建模的重要性并使用这些模型作为软件设计的基础来解决领域复杂性。

领域驱动设计的核心是:

  • 将主要焦点放在核心领域上。
  • 基于领域模型的复杂设计
  • 建立技术专家和领域专家之间的协作。

近年来,领域驱动设计的需求愈发迫切。基于微服务和云计算的架构已导致系统由众多以复杂方式交互的小组件组成。如果没有清晰且定义明确的领域模型来指导其设计,此类系统很快就会变成“一团泥球”。

在本文中,我们将了解领域驱动设计的基础知识及其关键概念,这些概念可以帮助我们构建与核心领域和业务逻辑一致的更易于维护和扩展的系统。

img

领域驱动设计的核心原则

领域驱动设计 (DDD) 专注于创建与底层业务领域紧密结合的软件系统。

它旨在通过将领域模型置于开发过程的中心来弥合技术实现和业务需求之间的差距。

DDD 有三个核心原则:

  • 根据领域专家的输入创建丰富的领域模型
  • 使用基于领域模型的通用语言
  • 从领域模型驱动软件设计

让我们更详细地探讨每个原则。

创建丰富的领域模型

DDD 的基础在于创建一个丰富的领域模型,该模型可以准确捕捉问题领域的关键概念、关系和业务规则。该模型不是由开发团队单独创建的,而是通过与对业务有深入了解的领域专家密切合作而产生的。

将领域知识提炼为可用模型的过程称为知识消化。

在此过程中,开发团队与领域专家密切合作,以确定和完善与问题最相关的概念和规则。这种协作工作通常包括集思广益、试验不同的模型设计,以及根据开发过程中获得的反馈和见解进行迭代改进。

例如,在银行系统中,与财务部门领域专家进行的知识会议将重点了解账户、客户、交易和利息计算等核心概念。

目标是创建一个模型来捕捉这些基本概念及其关系,这些模型可以被领域专家理解,并可作为软件系统的实践基础。

使用通用语言

随着领域模型的成型,开发团队和领域专家会形成一种共同语言。DDD 将此称为通用语言,因为它渗透到项目的各个方面。

img

通用语言直接基于领域模型。

代码中的类、方法和变量的名称均源自模型概念。同样,在讨论和文档中,团队使用通用语言的术语和短语,以确保清晰的沟通并避免歧义。

通过一致使用通用语言,代码直接反映模型,使得模型更容易理解和修改。

例如,如果银行领域模型包含“帐户”、“存款”和“取款”等概念,那么这些相同的术语将在代码、开发人员和领域专家之间的讨论以及项目文档中一致使用。当每个人都使用相同的语言时,任何理解上的歧义或不一致之处都会很快显现出来。

这是一个简单的代码示例,演示了在编写类及其方法时如何使用通用语言。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Account {
    private String accountNumber;
    private double balance;
    public void deposit(double amount) {
        // Perform deposit logic
        balance += amount;
    }

    public void withdraw(double amount) {
        // Perform withdrawal logic
        if (balance >= amount) {
            balance -= amount;
        } else {
            throw new InsufficientFundsException("Insufficient funds for withdrawal");
        }
    }
}

领域模型到软件设计

在 DDD 中,领域模型不仅仅是一个概念工具。它是软件设计的基础。软件的结构和行为反映了模型的结构和行为。

这种方法称为模型驱动设计。

在实践中,这意味着代码中的类、关系和行为直接对应于领域模型中的概念、关系和规则。设计不是由技术考虑或基础设施细节驱动,而是由有效表达领域模型的需求驱动。

例如,如果银行领域模型定义了一个“帐户”概念,其中包含“存款”和“取款”等行为,那么软件设计将包括一个“帐户”类,其中包含“存款”和“取款”方法。这些方法如何工作的实现细节将由模型中捕获的规则和要求指导。我们已经在上一节中看到了这一点。

构建领域模型的关键模式

构建领域模型是领域驱动设计中最重要的方面。然而,一些关键的模式和实践可以帮助我们实现最佳结果。

让我们详细了解一下其中的主要内容。

分层架构

在领域驱动设计 (DDD) 的背景下,分层架构有助于隔离领域模型并促进明确的关注点分离。

下图显示了应用程序如何构建成层。

img

通常,分层架构由四个主要层组成:

  • 用户界面(表示)层: 此层负责向用户呈现信息并解释用户命令。它与应用层通信以发送用户请求并接收响应。
  • 应用层:应用层协调整体应用程序活动并协调 UI 和域层之间的数据流。它不包含业务逻辑,但将其委托给域层。应用层处理特定于应用程序的任务,例如事务管理和安全性。
  • 领域层:领域层是软件系统的核心,也是领域驱动设计的主要关注点。领域层表示问题领域内的真实概念、实体及其关系。换句话说,它封装了有关业务领域的信息,并包含业务规则和逻辑。
  • 基础设施层:基础设施层提供通用技术能力,支持架构的更高层。它包括持久性、消息传递、日志记录和其他技术服务等跨切关注点。

例如,在电子商务系统中,领域层将包含产品、订单和客户等核心业务概念。UI 层将具有产品展示、购物车和结账的屏幕。应用层将处理订单流程。基础设施层将提供持久性、消息传递等功能。

实体

在领域驱动设计 (DDD) 中,实体是一个基本概念,表示具有独特身份和连续性线索的对象。

实体具有几个关键特征,可以将其与域中的其他对象区分开来:

  • 实体由其身份而非属性集合定义。身份使实体独一无二。
  • 实体可以随着时间改变其状态和属性,同时仍然保持其身份。
  • 实体的身份用于在系统内跟踪和引用实体。其他实体可以根据实体的身份与实体建立关系。

要理解实体的概念,请考虑银行系统。在这种情况下,BankAccount 是实体的一个典型示例。

每个银行账户都有一个唯一的账号作为其身份。无论不同账户之间可能具有什么共同属性,这个账号都可以将一个账户与另一个账户区分开来。

img

银行账户的信息可能会随着存款、取款或账户持有人信息更新而发生变化。尽管发生了这些变化,但账户仍保留其身份。在整个生命周期内,账户始终是同一个账户。

值对象

值对象代表域的描述性方面,但没有自己的身份。它们仅由其属性定义,而不是具有与其他对象区分开来的单独身份。

值对象的一些关键特征如下:

  • 与实体不同,值对象没有唯一标识。它们完全由其属性定义。
  • 值对象是不可变的,这意味着它们的状态在创建后无法修改。更改值对象意味着创建具有所需属性值的新实例。
  • 任何两个具有相同属性值的值对象都被认为是相等且可以互换的。

例如,在客户实体中,表示特定位置的地址是值对象的典型示例。它由街道、城市和邮政编码等属性定义。此外,任何两个具有相同详细信息的地址都被视为同一地址。

img

其他一些值对象示例是“MonetaryAmount”、“GeographicLocation”、“Color”等。

服务

服务是一个概念,表示不自然地属于实体或值对象职责的操作或行为。它们封装了不属于任何特定对象但仍在领域模型中发挥重要作用的逻辑。

服务的主要特征如下:

  • 服务对实体和值对象进行操作,但它们没有影响其行为的内部状态。
  • 服务的接口使用其他元素进行定义。例如,服务以实体或值对象作为输入,对其执行操作,并可能返回其他实体或值对象作为输出。
  • 服务是无状态的。换句话说,任何服务实例都可以被任何客户端使用,而不会影响其他客户端。

例如,航空预订系统可以有一个“FlightSchedulingService”,负责根据提供的标准查找可用的航班。此服务中的方法可以将出发机场和目的地机场作为输入,并返回可用航班列表。这样的操作自然不属于任何单个实体,例如“机场”或“航班”,因为它涉及协调来自多个来源的信息并应用业务规则。

img

聚合

在领域驱动设计 (DDD) 中,聚合是一种通过将相关对象分组为一个有凝聚力的单元来帮助管理领域模型的复杂性的模式。

它们具有几个主要特征,例如:

  • 聚合定义了一组相关对象的一致边界。这意味着聚合内的对象始终处于彼此一致的状态。
  • 每个聚合都有一个指定的根实体,称为聚合根。根实体是聚合中唯一可供外部对象引用的成员。与聚合内部对象的所有交互都必须通过根实体。
  • 聚合应该设计得足够小以确保一致性和可维护性,但又要足够大以包含所有必要的相关对象。

为了说明聚合的概念,让我们考虑一个订单处理系统中的例子。

在此示例中,“订单”及其关联的“订单项”形成自然聚合。“订单”封装了与订单处理相关的业务规则和不变量,例如计算总金额、应用折扣以及确保订单项的有效性。

img

从代码角度来看,在处理 LineItem 时,Order 聚合可能如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Order {
    private String orderId;
    private List<LineItem> lineItems;
    private BigDecimal totalAmount;
    private OrderStatus status;
    public void addLineItem(LineItem lineItem) {
        lineItems.add(lineItem);
        totalAmount = totalAmount.add(lineItem.getSubtotal());
    }

    public void removeLineItem(LineItem lineItem) {
        lineItems.remove(lineItem);
        totalAmount = totalAmount.subtract(lineItem.getSubtotal());
    }
}

public class LineItem {
    private String productId;
    private int quantity;
    private BigDecimal unitPrice;
}

外部对象(例如“客户”或“付款”)将仅引用“订单”实体,而不直接引用“LineItems”。对“LineItems”的任何操作或修改(例如添加、删除或更新项目)都将通过“订单”实体执行。

存储库

存储库是一种模式,它为特定类型的实体或聚合根提供持久层上的抽象。它充当类似集合的接口,封装底层数据访问和存储机制。

关于存储库的几个要点如下:

  • 存储库提供模拟集合行为的方法,例如添加、移除和查询对象。客户端代码可以像与简单集合一样与存储库交互,而无需暴露底层持久性机制的复杂性。
  • 存储库充当特定聚合或实体的持久层的网关。
  • 存储库将应用程序和领域设计与持久层的细节分离。

例如,“CustomerRepository”将提供诸如add(customer)、remove(customer)和findByID(id)等方法来管理客户实体的持久性,而不公开存储方法的细节。

img

这些方法允许客户端代码以模仿“客户”对象集合的方式与“CustomerRepository”进行交互。

客户端可以添加、删除和查询客户,而无需担心数据如何存储在数据库中。在后台,存储库可以使用 ORM 工具、SQL 查询或任何其他持久性机制与底层存储系统进行交互。

工厂

在领域驱动设计中,工厂是一种封装复杂实体或聚合创建的模式。它为客户端提供了一个创建对象的接口,而无需暴露创建过程的细节或所涉及的依赖关系。

工厂的一些主要特征如下:

  • 工厂隐藏了对象实例化、验证和处理依赖关系的复杂性。
  • 工厂允许独立于使用它们的客户端更改创建过程。这提高了系统的灵活性和可维护性。
  • 工厂确保它们创建的对象始终处于一致且完全初始化的状态。

例如,电子商务系统中的“OrderFactory”可以获取所有必要的详细信息(客户、商品、数量等),创建“Order”和“LineItem”实例,将它们适当地链接在一起,并将生成的订单汇总返回给客户端。

img

DDD 的战略设计概念

在处理大型项目中的大型模型时,应遵循一些战略设计理念,以获得领域驱动设计的好处。

有界上下文

在大型软件项目中,领域模型通常包含多个子域,每个子域都有自己的一组概念、实体和业务规则。

尝试在所有这些子域中维护单一、统一的模型可能会导致模型复杂且混乱,并且随着时间的推移会变得难以理解、推理和维护。

解决这一挑战的方法是引入“有界上下文”的概念。

有界上下文是领域模型中的特定区域,代表特定子领域或一组相关概念。每个有界上下文都封装了其统一模型,该模型根据该子领域的特定需求和要求量身定制。

定义良好的有界上下文具有以下特征:

  • 统一模型:在有界上下文中,应该有一个一致且连贯的模型来表示特定于该子域的概念、实体和关系。
  • 通用语言:每个有界上下文都应该有其通用语言。
  • 明确的范围和边界:有界上下文应该具有明确定义的范围和边界。

为了理解有界上下文的概念,我们来看一下电子商务系统的例子。在这个系统中,我们可以识别出几个子域,每个子域都可以是一个单独的有界上下文:

  • 目录上下文:此上下文专注于管理产品目录,包括产品信息、类别和属性。它有自己的模型来表示产品、产品变体及其之间的关系。
  • 订单上下文:订单上下文处理下订单和管理订单的过程。它有一个模型,表示订单、订单项以及订单处理和履行的相关业务规则。
  • 计费环境:计费环境处理电子商务系统的财务方面,包括开票、付款和退款。它有一个表示发票、付款交易和相关业务规则的模型。
  • 履行环境:履行环境负责订单的物流和交付。它有一个模型,表示运输方式、跟踪信息和履行订单所涉及的流程。

img

有界上下文之间的关系

不同的有界上下文之间可能存在关系和交互。这些关系通常通过不同上下文的模型之间的共享概念或翻译来表示。

建立有界上下文之间关系的一些关键模式如下:

1 - 上下文映射

在复杂的领域驱动设计 (DDD) 项目中,多个有界上下文代表系统的不同子域或部分。理解整个系统格局可能会变得困难。创建上下文映射将大有裨益。

上下文图是项目中的有界上下文及其之间关系的高级可视化表示。它提供了系统的鸟瞰图,突出显示了关键边界和集成点。

上下文图应该提供以下内容的清晰概述:

  • 有界上下文:系统内不同的子域或逻辑边界,每个子域或逻辑边界代表领域模型的特定区域。
  • 关系:有界上下文之间的连接和交互,表明它们如何相互关联和通信。
  • 集成点:有界上下文交换数据或协调其行为的集成的关键点。

2 - 共享内核

共享内核代表多个团队或有界上下文常用和依赖的领域模型的一部分。它封装了领域中的核心概念、实体及其关系,这些概念、实体及其关系与系统的不同部分相关。

例如,假设有两个有界上下文 - 销售和履行。销售有界上下文包括产品目录、定价、促销、购物车和订单下达。履行有界上下文处理库存、供应商、装运和交付。

虽然这些有界上下文关注的重点各不相同,但“订单”是它们共同的一个概念。“订单”源自销售有界上下文,但一旦下达,就需要由履行有界上下文来履行。因此,订单构成了销售和履行之间的共同核心。两个团队需要就“订单”的含义及其生命周期达成一致。

img

3 - 反腐败层

当将新系统与具有不兼容或不良模型的遗留系统或外部合作伙伴的系统集成时,保护新系统模型的完整性和纯度至关重要。

这就是防腐层模式发挥作用的地方。

防腐层充当新系统与现有系统或外部合作伙伴系统之间的绝缘层。它提供了一种转换机制,使新系统能够与其他系统进行交互,而不会损害其自身的模型和设计原则。

img

工作原理如下:

  • 新系统将请求发送到反腐败层,而不是直接与遗留系统或外部系统交互。
  • 反腐层接收新系统的请求,并将其转换为与原有系统或外部系统模型兼容的格式。它使用这些转换后的请求与外部系统进行通信。
  • 当外部系统响应时,防腐层会将响应转换回与新系统域模型一致的格式,然后才让其通过。

概括

在本文中,我们学习了领域驱动设计的基础知识以及它如何帮助您基于核心领域设计系统。

让我们简要总结一下所学内容:

  • DDD 专注于创建丰富的领域模型,以反映对业务领域的深刻理解。领域模型基于领域专家的输入,并作为软件的概念框架。
  • 基于领域模型,可以开发出通用语言。开发人员和领域专家在所有交流中(对话、文档和代码本身)都一致使用这种语言。使用通用语言有助于保持模型和实现的一致性。
  • 软件设计由领域模型驱动,而不是技术问题。这称为模型驱动设计。
  • DDD 通过将领域层与基础设施和用户界面问题隔离并将大型模型划分为有界上下文来管理复杂性。
  • 为了构建领域模型,DDD 利用了多种模式,例如分层架构、实体、值对象、服务、聚合、存储库和工厂。
  • 对于更复杂的领域,可以利用有界上下文之类的战略设计模式。
  • 为了管理多个有界上下文之间的关系,使用了上下文映射、共享内核和防腐层等技术。