DDD 领域驱动设计

DDD 领域驱动设计概念

什么是 DDD?

DDD(领域驱动设计,Domain-Driven Design) 是一种软件开发方法论和设计思想。DDD 通过领域驱动设计方法定义领域模型,从而确定业务和应用的边界,保证业务模型和代码模型的一致性。

因为 DDD 主要应用在微服务架构场景,所以想要更好的理解 DDD 的概念,需要结合微服务架构来看:

  • DDD 是一种设计思想,确定业务和应用的边界
  • 微服务架构需要 将系统拆分为多个小而独立的服务

微服务的拆分一直是业界的一个难题:微服务拆分的粒度应该多大?服务到底应该如何拆分?服务之间的边界如何定义?

有人可能认为,微服务不就是拆就完事了?不需要管那么多!实际上微服务的拆分是门 “艺术”:

  • 服务拆分的太细,项目复杂度会过高,接口的调用成本、服务运维成本大幅上升。
  • 服务拆分的太粗,业务边界变得模糊,服务的耦合度还是过高,失去了微服务的优势。

而 DDD 就是一个方法论,指导我们根据领域模型确定业务的边界,从而划分出应用的边界,最终落实成服务的边界、代码的边界。

本项目虽然没有涉及到微服务,但是不妨碍利用 DDD 思想拆分代码架构。最终想要变成微服务架构仅需抽离包中的代码独立部署即可。

DDD 的目标
  1. 通过领域模型实现业务需求:开发者与领域专家共同理解业务需求,形成共享语言并构建模型。
  2. 提高系统的灵活性与可维护性:通过合理划分限界上下文,减少系统的耦合度,使得不同模块或子系统可以独立演化。
  3. 支持复杂业务逻辑的表达:通过深入的业务建模,使得复杂的业务逻辑能够清晰、准确地反映在代码中。

总结一下,就是让系统更贴合业务,让大型系统更利于独立建设和维护。

DDD 的适用场景
  • 业务复杂的系统:如金融系统、电商平台等,涉及的业务逻辑复杂且频繁变化。
  • 需要与多个部门或团队合作的项目:DDD 强调跨部门协作,适用于多方参与的大型项目。
  • 长周期、长期维护的项目:DDD 强调可维护性与演化,适合需要长期维护和扩展的系统。

总结一下,大型的、跨部门协作的、长期维护的复杂项目。

DDD 的建设

DDD 会先建立领域模型,根据业务划分领域边界,进而确定微服务的边界,然后再根据领域分块编码实现。

实际上 DDD 的建设包括 战略设计战术设计 两部分。

战略设计

从业务出发,建立领域模型,统一限界上下文。

设计时,需要先进行事件风暴(类似于头脑风暴),邀请领域专家、架构师、开发人员、测试人员、产品经理、项目经理等团队人员一起参加讨论。

描述个场景,大家在会议室里,搞一个大白板,参与者们将自己的想法和意见写在贴纸里并罗列到白板上,大家 先发散思维 进行讨论、记录。

主要讨论的内容是:系统会涉及哪些业务,哪个业务动作会触发另一个业务的什么动作,其间的输入是什么?输出是什么?

通过这类分析把所有的业务、业务行为、业务结果都罗列出来,拆分出领域模型中的事件、命令、实体等领域对象。然后梳理这些领域对象之间的关系,从不同维度进行聚类,形成聚合、聚合根、限界上下文等,这个过程就是 收敛。 限界上下文可以简单理解为微服务的边界,将其映射到代码模型,就完成了微服务的拆分。

💡 事件风暴实际上会利用常见的产品设计和用户体验分析方法,比如:

  • 用例分析:对系统功能需求进行描述,以确定系统如何与外部参与者(即用户或其他系统)进行交互
  • 场景分析:通过设定具体的情境或情景,来探讨用户如何在不同的环境下使用产品或系统
  • 用户旅程分析:从用户的角度,描绘用户在使用产品或服务的过程中,从开始到结束的一系列步骤或行为
战术设计

从技术实现出发,将领域模型和代码模型进行映射Kzvg0=

这个阶段就是完成代码落地,包括聚合、聚合根、实体、值对象等代码逻辑的设计与实现。

DDD 体系名词解析

1、领域

领域指系统关注的业务领域或问题空间,具体的领域与公司或组织的核心业务有关。

实际上在 DDD 中 领域就是用来确定范围,而范围就是边界。

一个领域又可以分为多个子领域,每个子领域代表系统的一部分业务。

而子域根据重要程度和功能特性,可划分为:

  • 通用域:指系统中一些通用的、不特定于某一业务的领域,它们在多个不同领域或系统中都有应用。(例如支付、日志管理)
  • 支撑域:指在系统中起到支持作用,但并不是直接驱动业务价值的部分(例如网关)
  • 核心域:指系统中最关键的部分,是业务的核心竞争力所在,能够为企业带来最大的价值

💡 这里需要注意,在不同业务(公司中)三类子域是有区别的,例如在普通公司中需要调用第三方支付,那么支付是通用域,但是对于支付公司(例如支付宝)来说支付是它们的核心域。

2、限界上下文

是指一个明确的边界,规定了某个子领域的业务模型和语言,确保在该上下文内的术语、规则、模型不与其他上下文冲突。

在事件风暴讨论过程中,我们需要完成通用语言的统一。例如电商场景下,我们统一叫物品为商品、将用户购买商品的行为叫下单。

我们都知道语言需要有语义环境。不同语义环境下,同一个语言表达的意思是不同的。比如:

  • “我吃得很饱,现在不能动了”:这里的“吃得很饱”表示的是 “吃到肚子很满”,字面意思是 “我已经吃得很饱了,吃不下了”
  • “我吃得很饱,今天的演讲让人充实”:这里的 “吃得很饱” 并非字面上的 “吃得饱”,而是比喻 “得到了很大的满足”,表现出内心的充实感。

而限界上下文实际上就类似于语义环境。通用语言需要业务边界,限界上下文就是定义了业务的边界,也就是领域的边界。

电商语义下称之为商品的东西,到运输语义下它就变成了货物。因此我们需要明确限界上下文,在这个上下文中团队内部人员对某一领域对象、领域事件的认知是一致的、没有歧义的。

3、实体

一般业务对象,且具有唯一标识对象都是实体。在代码中所谓的唯一标识就是 ID,例如,订单有订单 ID,用户有用户 ID,它们都是典型的实体。

实体的关键点就在于唯一标识,随着生命周期的变化,实体中的属性可能会改变,例如订单可以从未完成变成已完成,但是其 ID 不会改变。

实体映射到代码中就是实体类。通常采用 充血模型 来实现,即与这个实体相关的所有业务逻辑都写在实体类中。

非常典型的值对象就是地址。比如用户实体对象有地址这个属性,那么这个地址就是值对象,它没有唯一标识,且创建后就不允许修改其本身的值。如果用户需要修改地址,那么这个属性是被整体替换的(换新的地址值对象)。

拥有这样特性的对象,就是值对象。

💡 实体和值对象并不是一成不变的,比如对电脑主机来说,显卡是一个值对象,显卡坏了就换一个,而对显卡厂商来说,显卡是实体,它们有编号需要追踪和管理的。

5、聚合

实体和值对象是基础的领域对象,聚合将多个实体和值对象组合成一个整体,实现高内聚低耦合。

简单来说实体和值对象是个体,个体与个体之间的合作需要被“领导”,而聚合就是将它们组织起来协同工作,这样才能保证聚合内数据的一致性(组织统一口径)。它可以作为微服务拆分的最小单位。

聚合还是数据修改和持久化的基本单位,实现数据的持久化存储。

6、聚合根

聚合根就好比聚合内的带头人,聚合内的多个实体不会直接对外提供接口访问,而是由聚合根统一提供对外接口。

一个聚合内只会有一个聚合根,聚合根通过对象引用的方式组织聚合内的实体和值对象,聚合根之间的合作是通过 ID 关联的。

这里需要注意:聚合根也是一个实体,也具有业务属性和业务逻辑和唯一标识。

例如订单域内只有订单和订单子项两个实体,那个订单就是这个域中的聚合根。

7、领域服务

聚合根可以实现跨多个实体的复杂业务行为,但是为了实现高内聚和低耦合,聚合根内部应该更聚焦与自身强关联的业务行为,复杂的跨多实体的业务可以放在领域服务中实现。

领域服务是指那些 不能归属于某个单一实体或值对象,但又属于领域模型的一部分 的业务逻辑。领域服务封装了对领域对象进行操作的核心业务规则,通常用于处理跨多个实体的操作,或者当业务逻辑无法直接归属于某个特定聚合时。

例如一个订单系统,需要处理订单支付功能,而支付涉及订单、用户账户、支付信息等多个实体,这个支付操作不太好归属某个实体,这样的逻辑就可以放到领域服务中。

public class PaymentService {
    public void processPayment(Order order, PaymentDetails paymentDetails, Account account) {
        // 处理支付逻辑
        // 调用多个实体方法来处理支付过程
    }
}

那聚合根更适合怎样的跨实体的业务呢?

例如你有一个“订单”聚合,其中包含订单条目、支付信息等,Order 作为聚合根,负责管理订单条目和确保订单的完整性。你不能直接访问订单条目(如 OrderItem),必须通过 Order 聚合根来进行操作。

public class Order {
    private List<OrderItem> items;
    private PaymentDetails paymentDetails;

    public void addItem(OrderItem item) {
        // 检查商品数量、价格等业务规则
        this.items.add(item);
    }

    public List<OrderItem> getOrderItems() {
        //....
    }

    // 其他聚合内部的一致性校验
}
DDD 建模总结

结合上面的名词解析,我们回顾一下 DDD 建模的流程。

首先我们需要领域建模,此时会进行事件风暴,通过用例分析、场景分析等方式列出所有的业务行为与事件,找出产生这些行为的领域对象,包括实体与值对象。梳理这些领域对象之间的关系,从实体中找出聚合根,再根据聚合根的业务,找寻与其业务紧密关联其它实体与值对象,从而形成聚合。多个聚合之间根据业务相关性又可以划出限界上下文。

可以通过 “开公司” 的比喻来帮助大家理解 DDD。领域就像公司的行业,决定了公司所从事的核心业务;限界上下文是公司内部的各个部门,每个部门有独立的职责和规则;实体是公司中的员工,具有唯一标识和生命周期;值对象是员工的地址或电话等属性,只有值的意义,没有独立的身份;聚合是部门,由多个实体和值对象组成,聚合根(如部门经理)是部门的入口,确保部门内部的一致性;领域服务则是跨部门的职能服务,比如 HR 或 IT 服务,为各部门提供支持和协作。

DDD 架构设计

充血模型和贫血模型

贫血模型和充血模型是两种面向对象设计模式,用于描述对象的职责划分和对象是否包含行为逻辑。

我们常见的对象内部的实现非常简单,仅包含数据属性和简单的 getter/setter 方法(充血模型),换句话说,这些对象是一个纯粹的“数据容器”,它仅负责保存数据,而不包含任何业务行为。

从领域模型设计角度来说,这样的设计称为贫血模型,偏向于传统分层架构的设计;与之对应的是充血模型,强调面向对象的系统设计。

两种模型的分类本质是对领域对象中 “数据与行为的职责划分” 的不同理解。反映了在软件设计中,如何组织领域对象的数据和行为,以及如何分配业务逻辑的不同设计思路。

充血模型是指领域对象不仅包含数据(属性),还包含处理这些数据的业务逻辑。换句话说,充血模型的领域对象是“充血”的,它们不仅有状态(数据),还有行为(业务方法)。

贫血模型则是指领域对象仅包含数据,不包含任何业务逻辑,所有的业务逻辑都放在单独的服务类中(通常是应用层或领域服务层)。领域对象本身是 “贫血” 的,只有状态,没有行为。

总结来看:

  • 充血模型 适合复杂业务,业务逻辑和数据紧密结合,符合面向对象设计的原则。
  • 贫血模型 适合简单业务,关注点分离,数据和业务逻辑分开,领域对象仅负责存储数据,服务类负责业务逻辑。

下面用代码举例,大家就知道它们的区别了。

代码示例

假设我们有一个订单系统,Order 是领域对象,包含了订单的状态和相关的业务逻辑。

1)充血模型代码示例

在充血模型中,Order 对象包含了业务逻辑(如 pay 和 cancel 方法),这些方法对订单的状态进行操作,直接将数据和行为结合在一起

public class Order {
    private String orderId;
    private double totalAmount;
    private boolean isPaid;

    public Order(String orderId, double totalAmount) {
        this.orderId = orderId;
        this.totalAmount = totalAmount;
        this.isPaid = false;
    }

    public void pay() {
        if (this.isPaid) {
            throw new IllegalStateException("Order is already paid");
        }
        this.isPaid = true;
    }

    public void cancel() {
        if (this.isPaid) {
            throw new IllegalStateException("Cannot cancel a paid order");
        }
        // Perform cancellation logic
    }

    public boolean isPaid() {
        return isPaid;
    }

    public double getTotalAmount() {
        return totalAmount;
    }
}

2)贫血模型代码示例

在贫血模型中,Order 对象只包含数据(状态),而所有的业务逻辑(如 payOrder 和 cancelOrder)都被移到了外部的 OrderService 服务类中。

public class Order {
    private String orderId;
    private double totalAmount;
    private boolean isPaid;

    public Order(String orderId, double totalAmount) {
        this.orderId = orderId;
        this.totalAmount = totalAmount;
        this.isPaid = false;
    }

    public String getOrderId() {
        return orderId;
    }

    public double getTotalAmount() {
        return totalAmount;
    }

    public boolean isPaid() {
        return isPaid;
    }

    public void setPaid(boolean paid) {
        isPaid = paid;
    }
}

public class OrderService {
    public void payOrder(Order order) {
        if (order.isPaid()) {
            throw new IllegalStateException("Order is already paid");
        }
        order.setPaid(true);
    }

    public void cancelOrder(Order order) {
        if (order.isPaid()) {
            throw new IllegalStateException("Cannot cancel a paid order");
        }
        // Perform cancellation logic
    }
}
二者对比

特点

贫血模型

充血模型QAI=

封装性

数据和逻辑分离

数据和逻辑封装在同一对象内

职责分离

服务类负责业务逻辑,对象负责数据

对象同时负责数据和自身的业务逻辑

适用场景

简单的增删改查、DTO 传输对象

复杂的领域逻辑和业务建模

优点

简单易用,职责清晰

高内聚,符合面向对象设计思想

缺点

服务层臃肿,领域模型弱化

复杂度增加,不适合简单场景

面向对象原则

违反封装原则

符合封装原则

在实际项目中,贫血模型和充血模型并非互相排斥。通常可以结合两者的优点:

  • 使用充血模型作为领域模型,封装复杂的业务逻辑。
  • 使用贫血模型作为数据传输对象(DTO),在系统之间传输数据。

DDD 的分层架构

在领域驱动设计(DDD)中,分层架构模型是一种常见的设计模式,用于组织和管理系统的复杂性。通过将应用分为不同的层次,每一层都有清晰的责任和角色,从而促进了代码的高内聚、低耦合和可维护性。

DDD 的分层架构主要有四层:用户接口层应用层领域层基础设施层。每层负责不同的职责,协调工作以实现系统的整体功能。

除基础设施层外,严格来说每层只能与 直接下层 产生依赖,即领域层只能被应用层调用,应用层只能被用户接口层调用。

当然也有 松散分层架构,层与层之间的依赖和交互更加灵活,不严格分隔。适用于快速开发,但随着系统复杂度的增加,可能变得难以维护。

难以维护。


1)用户接口层

也叫表示层或 Web 层,主要负责与外部(用户、API 等)的交互。它的主要职责是接收用户输入并返回系统的输出。表示层不包含业务逻辑,而是将用户的请求转发到应用层处理,并将处理结果返回给用户。

2)应用层

应用层主要用来协调领域层的逻辑和基础设施层的资源。应用层不包含业务规则或业务逻辑,但会调用领域层的服务进行服务编排与组合,来实现特定的业务。

如果有对其他服务的远程调用,也放在这层实现。除此之外,权限校验、事务、事件等操作也都可以放在这层进行实现。

3)领域层

领域层是整个架构的核心,包含了应用的业务逻辑、规则和策略。它定义了核心的领域模型,包括聚合根、实体、值对象、领域服务等。

领域层的目的是将业务需求转化为代码,并确保业务规则在应用中得以执行。该层的设计强调与业务领域的紧密耦合,是 DDD 中的重点。

4)基础设施层

基础设施层提供技术支持和持久化服务,采用依赖倒置设计,封装基础资源。负责与外部系统(如数据库、消息队列、缓存等)的交互。基础设施层的主要职责是实现应用层和领域层所需要的技术服务,如数据存储、邮件发送、日志记录等等。

依赖倒置设计实际上指的是各层对基础资源(如数据库)仅依赖其接口而不是具体的实现,假设后续替换基础资源(数据库),仅需替换具体实现,不需要修改各层依赖的代码。

三层架构到 DDD 四层架构的转化

三层架构是传统的架构模式,结合 SpringMVC 通常由以下三层组成:

  • 表示层(Controller 层):处理 HTTP 请求,调用业务层的服务,返回视图或数据。
  • 业务逻辑层(Service 层):封装核心业务逻辑,执行业务操作。
  • 数据访问层(Repository 层):负责与数据库交互,执行数据的持久化和查找。

转化 DDD 四层架构映射关系如下图所示:

主要改造点就是业务逻辑层的 Service,根据聚合拆分到应用层的应用服务与领域层的领域服务,部分业务逻辑还会以充血模型下沉到 Entity 中。

接着就是数据访问层的改造,根据依赖倒置原则,数据库的访问接口会被放到领域层中(因为属于行为),具体的访问实现则是在基础设施层内(为行为提供支持)。除此之外,第三方工具、Common、Config 等都放在基础设施层中。

DDD 代码架构

首先明确一点,DDD 代码架构并没有统一的标准,不同公司的架构都是不一样的!但是核心的思想都是大差不差的,仅一些细节有调整。

按照四层架构,我们可以建立 interfaces(用户接口层)、application(应用层)、domain(领域层)、infrastructure(基础设施层) 这 4 个包。

interface 是 Java 关键字,因此包名加了个 s。

1、interface

该层主要负责与外部系统交互,包括用户界面(UI)、API接口、请求的接收和响应的返回等。它作为领域层与外部世界的接口,确保领域逻辑的解耦。

存放的代码:

  • 控制器(Controller):处理HTTP请求,负责路由和请求的转发。
  • REST API 接口:定义暴露给外部系统的服务接口。
  • 请求和响应对象:用于与外部系统交换数据。
2、application

该层负责协调多个领域对象的操作,完成应用级的任务。它充当领域层与用户接口层之间的桥梁,调用领域层中的业务逻辑,并将结果返回给用户接口层。应用层的职责是实现具体用例,而不包含业务规则。

存放的代码:

  • 应用服务(Application Service):负责组织和协调领域对象,处理跨多个聚合的操作,通常表示应用中的具体功能,如“下订单”或“注册用户”。
3、domain

该层包含核心业务逻辑,它是系统的核心部分,负责模型的定义和业务规则的实现。领域层中的模型代表着业务概念,通常会包括聚合、实体和值对象。这个层不依赖于任何外部技术或框架,它专注于业务本身。

存放的代码:

  • 聚合:一个聚合由多个实体和值对象构成,它们之间有着一致的业务规则,一般包名就代表一个聚合
  • 实体:具有唯一标识符(ID)的对象。
  • 值对象:没有身份标识且是不可变的对象,通常用于表示某个概念的属性。
  • 领域服务:当某个业务逻辑无法归属到某个实体或聚合时,使用领域服务来封装这些业务逻辑。
  • 领域事件:表示领域中发生的某个重要事件,如“订单已支付”。
  • 仓储接口:定义资源访问的接口
  • 持久化对象:PO(数据库查询逻辑不复杂时,可以省略)
4、infrastructure

该层提供技术支持,是所有其他层的基础设施。它包含数据库操作、消息队列、缓存、文件存储等第三方依赖。基础设施层实现了与外部系统的交互,但不包含业务逻辑。

存放的代码:

  • 持久化:如使用 JPA 或 MyBatis 等技术实现数据库的访问。
  • 外部系统集成:与外部服务或系统的通信,如调用文件存储。
  • 工具类和基础设施组件:提供诸如日志、定时任务、邮件发送等功能。
项目目录结构示例

main/java 包下:

  • application(应用层)
  • domain(领域层)
  • order(订单聚合)
  • entity(实体)
  • valueObject(值对象)
  • event(事件)
  • repository(仓储)
  • service(领域服务)
  • user(用户聚合)
  • infrastructure(基础设施层)
  • api(外部接口)
  • config(配置)
  • mq(消息队列)
  • repository(仓储实现)
  • facade(仓储接口)
  • po(持久化对象)
  • util(工具类)
  • interfaces(用户接口层)
  • assembler(对象转化类)
  • dto(传输对象)
  • controller(提供给用户界面、外部服务的接口)
  • shared(共享模块)
  • Application 项目主类(或启动类)

此外,实现 DDD 的过程中,还可能会用到工厂和仓储模式。

  • 工厂:用于创建聚合和实体,因为聚合根与聚合内的实体、值对象关系比较复杂,为了确保对象创建的一致性和完整性会使用工厂模式来创建领域对象(通常从数据库获取 PO 持久化对象后,通过工厂模式创建 DO 领域对象)。
  • 仓储:用于持久化领域对象(如实体和聚合),它封装了数据库操作,使得业务逻辑与数据存储分离。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值