一篇看懂 DDD 领域驱动设计

307 阅读13分钟

在电商系统中创建多规格商品时,你是否遇到过这样的场景?商品服务用三个字段描述 SKU,促销服务用 JSON 结构存储价格策略,订单服务却需要将商品信息扁平化为字符串。这种业务概念的割裂,正是传统开发模式难以应对复杂业务的缩影。领域驱动设计(DDD)的诞生,为这类问题提供了全新的解题思路。

DDD 核心思想

传统开发模式往往陷入 “数据库驱动” 的陷阱:先设计表结构,再填充业务逻辑,导致代码成为 SQL 的附庸。一个典型的贫血模型案例是,5000 行的 OrderService 类中堆砌着下单、支付、库存扣减等所有逻辑,而 Order 对象仅是包含 getter/setter 的数据载体。

DDD 通过领域模型重构这一关系:将 “订单支付成功” 的业务概念映射为 Order.pay () 方法,将 “库存不足” 的规则封装在 Inventory.check () 中。每个领域对象都像乐高积木,通过组合实现复杂业务逻辑。

核心概念

  • 通用语言:团队共同定义的业务术语词典。例如电商场景中,“履约” 代表从订单生成到商品送达的全流程,“逆向履约” 特指退货场景。大家基于这套通用语言交流,能避免因对业务术语理解不同产生的沟通障碍,提高协作效率。
  • 限界上下文:业务概念的势力范围。不同业务场景下,对同一概念的定义和操作范围可能不同。以 “商品” 为例,商品服务的 “商品” 包含类目属性、SKU 组合,营销服务的 “商品” 则关注促销价、活动标签。每个限界上下文都有自己独立的领域模型,相互之间通过明确的接口交互,这样可以避免不同业务场景下概念混淆,让系统架构更清晰。
  • 领域分层架构
    • 表现层:负责与用户交互,包括 REST API 提供对外接口,以及页面渲染展示给用户界面。例如电商网站的商品展示页面、用户下单操作界面等,都是表现层的范畴。它主要任务是收集用户请求,将请求传递给应用层,并将应用层返回的结果展示给用户。
    • 应用层:承担事务管理、事件发布等职责。比如在电商系统中,用户下单操作涉及到订单创建、库存扣减等多个操作,应用层要确保这些操作在一个事务中执行,保证数据一致性;当订单状态发生变化时,应用层负责发布相应的领域事件,通知其他相关服务。
    • 领域层:业务核心所在,包含实体、值对象、聚合根等。实体是具有唯一标识的对象,像订单(Order)以订单 ID 为标识,它具有生命周期和业务逻辑,比如订单可以有创建、支付、取消等操作;值对象是描述特征的无标识对象,如地址(Address),“北京朝阳区某地址” 作为一个值对象,主要用于描述实体的某个特征,它不可变,一旦创建其属性不能被修改;聚合根是保证业务完整性的边界,例如修改订单项(OrderItem)必须通过订单(Order)聚合根来进行,这样能确保整个订单业务的一致性和完整性。
    • 基础设施:提供底层支撑,像数据库用于存储数据,消息队列用于服务间异步通信。在电商系统中,订单数据存储在数据库中,而当订单状态变更时,通过消息队列将订单状态变更事件发送给库存、物流等相关服务,实现服务间解耦。

实战:DDD 在电商系统的落地

领域模型构建示例

以订单履约场景为例,详细展示相关实体、值对象和聚合根的代码设计:

// 订单ID,作为订单的唯一标识,使用值对象实现
public class OrderId {
    private final String value;
    public OrderId(String value) {
        this.value = value;
    }
    public String getValue() {
        return value;
    }
}
// 订单项,具有独立的业务逻辑,是实体
public class OrderItem {
    private String itemId;
    private String productId;
    private int quantity;
    private BigDecimal unitPrice;
    public OrderItem(String itemId, String productId, int quantity, BigDecimal unitPrice) {
        this.itemId = itemId;
        this.productId = productId;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }
    // 计算订单项总金额的业务逻辑
    public BigDecimal calculateAmount() {
        return unitPrice.multiply(BigDecimal.valueOf(quantity));
    }
    // 其他业务方法...
}
// 地址,作为描述订单特征的值对象
public class Address {
    private String province;
    private String city;
    private String district;
    private String detail;
    public Address(String province, String city, String district, String detail) {
        this.province = province;
        this.city = city;
        this.district = district;
        this.detail = detail;
    }
    // 由于是值对象,通常不提供修改方法,如需修改则创建新对象
    // 其他获取属性方法...
}
// 订单状态,使用枚举定义
public enum OrderStatus {
    CREATED, PAID, SHIPPED, DELIVERED, CANCELED
}
// 订单聚合根
public class Order {
    private OrderId id;
    private List<OrderItem> items;
    private Address shippingAddress;
    private OrderStatus status;
    public Order(OrderId id, List<OrderItem> items, Address shippingAddress) {
        this.id = id;
        this.items = items;
        this.shippingAddress = shippingAddress;
        this.status = OrderStatus.CREATED;
    }
    public void pay(Payment payment) {
        if (payment.getAmount().compareTo(this.total()) != 0) {
            throw new PaymentAmountMismatchException();
        }
        this.status = OrderStatus.PAID;
        this.registerDomainEvent(new OrderPaidEvent(this.id));
    }
    // 计算订单总金额
    public BigDecimal total() {
        return items.stream()
               .map(OrderItem::calculateAmount)
               .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    // 其他业务方法,如取消订单等...
    private void registerDomainEvent(OrderPaidEvent event) {
        // 这里可以实现将事件发布到事件总线的逻辑
    }
}

项目结构设计示例

以电商订单模块为例,展示基于 DDD 的项目结构:

order-module
├── api
│   ├── dto
│   │   ├── OrderCreateRequestDTO.java
│   │   ├── OrderResponseDTO.java
│   │   └── ...
│   └── controller
│       ├── OrderController.java
│       └── ...
├── application
│   ├── service
│   │   ├── OrderApplicationService.java
│   │   └── ...
│   └── event
│       ├── OrderPaidEvent.java
│       └── ...
├── domain
│   ├── model
│   │   ├── Order.java
│   │   ├── OrderItem.java
│   │   ├── OrderId.java
│   │   ├── Address.java
│   │   └── OrderStatus.java
│   ├── repository
│   │   ├── OrderRepository.java
│   │   └── ...
│   └── service
│       ├── OrderDomainService.java
│       └── ...
├── infrastructure
│   ├── repository
│   │   ├── impl
│   │   │   ├── OrderRepositoryImpl.java
│   │   │   └── ...
│   │   └── OrderRepository.java
│   ├── event
│   │   ├── EventBus.java
│   │   └── EventDispatcher.java
│   └── config
│       ├── DatabaseConfig.java
│       └── MessageQueueConfig.java
└── test
    ├── api
    │   ├── OrderControllerTest.java
    │   └── ...
    ├── application
    │   ├── OrderApplicationServiceTest.java
    │   └── ...
    ├── domain
    │   ├── OrderDomainServiceTest.java
    │   └── ...
    └── infrastructure
        ├── OrderRepositoryImplTest.java
        └── ...

在上述项目结构中:

  • api 包:负责对外提供接口,dto 子包存放数据传输对象,用于在表现层和应用层之间传递数据;controller 子包存放控制器类,接收外部请求并调用应用层服务。
  • application 包:应用层,service 子包中的应用服务类协调领域服务完成业务操作;event 子包定义领域事件。
  • domain 包:领域层核心,model 子包存放领域模型相关的实体、值对象、聚合根等;repository 子包定义领域模型的数据访问接口;service 子包存放领域服务,处理跨聚合的业务逻辑。
  • infrastructure 包:基础设施层,repository 子包的 impl 子包实现领域层定义的数据访问接口;event 子包实现事件总线和事件分发功能;config 子包存放数据库、消息队列等配置类。
  • test 包:存放各层的单元测试和集成测试代码,保证系统的正确性和稳定性。

限界上下文协作

通过领域事件实现服务间解耦:

订单服务发布 OrderPaidEvent 事件,库存服务监听事件并执行库存扣减,物流服务生成运单号。用流程图表示如下:

当订单服务完成订单支付操作,发布 OrderPaidEvent 事件到消息队列(MQ)。库存服务和物流服务从消息队列获取该事件,分别执行库存扣减和生成运单操作。这样各个服务之间不需要直接调用,降低了服务间耦合度,当某个服务业务逻辑发生变化时,对其他服务影响较小,提高了系统的可维护性和扩展性。

设计要素

  • 实体:具有唯一标识的对象,如 Order(订单 ID 为标识)。实体在业务流程中具有独立的生命周期和业务行为,例如订单实体可以经历创建、支付、发货、完成等不同状态变化,每个状态变化都伴随着相应业务逻辑处理。
  • 值对象:描述特征的无标识对象,如 Address(北京朝阳区某地址)。值对象主要用于描述实体的某些属性,它没有自己独立的唯一标识,通过属性值来区分不同实例,并且值对象通常是不可变的,这样可以保证数据的一致性和稳定性。例如在订单中,收货地址作为值对象,在订单整个生命周期中,如果地址信息不发生变化,就不需要额外的标识来区分不同地址实例。
  • 聚合根:保证业务完整性的边界,修改 OrderItem 必须通过 Order 聚合根。聚合根是一个聚合的核心对象,它对外提供统一的操作接口,内部维护聚合内各对象的一致性。比如在订单聚合中,订单项(OrderItem)不能被外部直接修改,必须通过订单(Order)聚合根来进行操作,这样可以确保订单相关业务逻辑的完整性,避免因直接操作订单项导致订单数据不一致等问题。
  • 领域服务:跨聚合的逻辑,如跨订单的优惠分摊计算。当电商系统有跨多个订单的优惠活动时,需要计算每个订单应分摊的优惠金额,这就涉及到多个订单聚合之间的逻辑处理,这种逻辑就可以封装在领域服务中。领域服务不隶属于任何一个特定聚合,它协调多个聚合完成复杂业务操作,使业务逻辑更加清晰和可维护。

三、DDD 与 MVC 的范式对比

传统三层架构往往演变为 “Service+DTO+DAO” 模式,导致业务逻辑碎片化。DDD 的革新之处在于:

维度MVC 架构DDD 架构
核心关注点请求响应流程业务概念完整性
业务逻辑存放位置Service 层领域对象内部
数据传递方式通过 DTO 在各层传递通过领域事件进行状态变更通知
可维护性修改可能影响多个 Service变更局限在限界上下文内

一个典型转变是:订单取消逻辑从 Service 层的 cancelOrder 方法,重构为 Order.cancel () 方法,包含库存释放、状态变更、通知生成等原子操作。在 MVC 架构中,订单取消逻辑可能分散在多个 Service 类中,各个 Service 类之间可能存在复杂的调用关系,当业务逻辑发生变化时,修改一处可能影响多个 Service 类;而在 DDD 架构中,订单取消逻辑封装在 Order 实体内部,相关操作都围绕 Order 对象进行,并且库存释放、状态变更等操作都在 Order.cancel () 方法中完成,这种设计使得业务逻辑更加集中,并且由于限界上下文的存在,当订单业务逻辑发生变化时,变更影响范围局限在订单限界上下文内,对其他业务模块影响较小,提高了系统可维护性。

DDD 的适用场景

理想应用场景

  • 保险理赔系统:涉及报案、定损、核赔、支付等多环节复杂流程。每个环节都有自己独特的业务规则和领域模型,例如报案环节涉及报案信息登记、报案人信息验证等;定损环节需要对损失进行评估、确定赔偿范围等。通过 DDD 可以将这些不同环节划分成不同限界上下文,构建各自领域模型,更好地处理复杂业务逻辑。
  • 医疗信息系统:需要严格区分问诊、检验、药房等子域。不同子域业务差异大,如问诊主要记录患者症状、医生诊断等信息;检验涉及各种检验项目、检验结果生成等。采用 DDD 能够针对每个子域进行独立建模和开发,使系统架构更清晰,满足医疗行业复杂业务需求。
  • 供应链金融平台:处理订单、物流、资金的多维度协同。订单涉及订单创建、跟踪;物流包括货物运输、仓储管理;资金涉及支付、结算等。各维度业务相互关联又有各自特点,DDD 通过限界上下文将这些业务解耦,实现多维度协同,保证业务完整性和一致性。

实施成本

对于日均订单量不足 1000 的初创电商,采用完整 DDD 可能过度设计。但当系统出现以下信号时,就是引入 DDD 的最佳时机:

  • 新需求开发需要修改 3 个以上 Service 类:说明现有系统业务逻辑耦合度高,不同功能模块之间相互影响大,新需求开发困难。引入 DDD 可以通过限界上下文将业务解耦,降低模块间依赖,提高开发效率。
  • 业务方频繁抱怨 “系统不够灵活” :表明系统难以快速响应业务变化。DDD 通过领域模型将业务逻辑与技术实现分离,当业务需求变更时,只需在领域模型层面进行调整,能更好地满足业务灵活性要求。
  • 代码库中出现多个重复的业务校验逻辑:体现现有系统代码缺乏合理的抽象和封装。DDD 通过实体、值对象、领域服务等概念将业务逻辑进行封装和复用,消除重复代码,提高代码质量。

在 Spring Cloud Alibaba 微服务体系中,DDD 的实施路径可能是:

  • 通过事件风暴工作坊划分限界上下文:团队成员通过头脑风暴,梳理业务流程中发生的事件,以及事件的触发者、参与者等信息,从而识别出不同业务领域,划分限界上下文。例如在电商系统中,通过事件风暴可以确定订单、商品、库存等不同限界上下文。
  • 定义各微服务的分层架构:参考前文提到的领域分层架构,每个微服务内部按照表现层、应用层、领域层、基础设施层进行分层设计。例如订单微服务,表现层负责接收和处理用户关于订单的请求,应用层协调订单业务流程,领域层实现订单相关业务逻辑,基础设施层负责订单数据存储等。
  • 使用 Nacos 实现配置 / 服务治理:Nacos 可以用于管理微服务的配置信息,以及服务的注册与发现。在 DDD 架构中,不同限界上下文对应的微服务可以通过 Nacos 进行配置管理,方便进行服务治理,例如动态调整服务参数、查看服务状态等。
  • 通过 Seata 保证跨服务的分布式事务:当业务操作涉及多个微服务,如订单创建涉及订单服务、库存服务等,需要保证分布式事务一致性。Seata 提供分布式事务解决方案,通过事务协调器、事务管理器等组件,确保跨服务操作要么全部成功,要么全部失败,维护数据一致性。
  • 用 Sentinel 实现领域服务的流量控制:Sentinel 可以对领域服务进行流量控制,防止因高并发请求导致服务雪崩。例如在电商促销活动期间,订单服务可能会收到大量请求,通过 Sentinel 可以设置限流规则,对进入订单服务的请求进行流量

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。

没准能让你能刷到自己意向公司的最新面试题呢。

感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。