DDD

DDD

DDD

DDD 是领域驱动设计(Domain-Driven Design)的缩写,它不是 MVC 工程结构,也不同于微服务架构,它是一种主要软件开发方法。

是一种以创建与业务领域紧密相关的软件模型,强调将复杂业务问题的建模放在第一位。

核心理念

领域模型(Domain Model)

领域模型是用面向对象的方式,将业务逻辑和业务状态封装成一个模型。 🤖

🌰举个例子:电商平台开发人员,业务提出需求:实现一个“用户下订单”的功能。

首先需要建模”订单“这个概念。必须询问清楚业务情况🤔:

  • 一个订单有哪些数据?(订单编号、总价、商品列表)
  • 一个订单可以做什么事?(支付、取消、发货)
  • 什么情况下能取消?(已支付就不能取消)
  • 取消后库存要恢复吗?

这些就是业务规则、业务状态。而不是只创建一个 Order 表,是把订单这个概念建模为一个“有数据 + 有行为”的模型。

Bounded Context

限界上下文就是某个模型的“语义边界”和“适用范围”。在这个边界内,模型的定义和业务规则是一致的、可控的,边界外则不是。🤖

🌰举个例子:“用户”这个词。

  • 会员系统 里,“用户”代表的是注册信息,如用户名、手机号、等级。
  • 支付系统 里,“用户”代表的是付款账号、支付密码、绑卡信息。
  • 客服系统 里,“用户”代表的是咨询记录、投诉等级、满意度。

这三个系统都叫 “User”,但环境语义不同。如果把这些都塞进一个 User 类,就丸辣😱。

聚合(Aggregate)

聚合是由一个“聚合根(Aggregate Root)”和它所管理的一组实体或值对象组成的业务一致性边界。

🌰举个例子:订单系统

用户下单时,一个订单会包含多个商品明细(OrderItem),每一项商品的价格、数量、折扣都是订单的一部分,但订单才是操作的主体。

Order 是聚合根,OrderItem 是被包含在聚合内部的实体,所有对 OrderItem 的操作都必须通过 Order 来完成(封装规则 + 保持一致性)。

聚合 ≠ 只是一组类

它不是简单的“把几个类写在一起”,而是具有业务边界和一致性要求的逻辑组合体。🤖

领域服务(Domain Service)

当某个业务操作不属于任何一个具体领域对象(实体、值对象)的职责时,就把它建模为领域服务。

换句话说:领域服务是用来表达“跨多个领域对象的业务行为”,它是“行为”,不是“数据”

什么叫做领域对象的业务行为?🤔

DDD 强调把“行为”塞进实体里(行为 + 状态 = 面向对象建模),但是:

  • 有些操作逻辑 不属于某个特定实体
  • 有些操作涉及多个聚合或实体,不能归谁所有
  • 有些行为是纯业务规则,但又不想放到 application 层或 util 里

🌰举个例子:我们不能把“支付”放到 UserOrder 里,因为:

  • 支付行为不属于用户的职责
  • 它需要协调订单、账户、支付渠道等多个对象

应用服务(Application Service)

应用服务是负责协调用户请求、执行业务用例、调用领域对象的服务层,它本身不承载业务规则。

DDD 分层图:

┌────────────────────┐
│   Interface      │ ← Controller, API 层
└────────────────────┘
         ↓
┌────────────────────┐
│Application Layer │ ← 应用服务:协调请求、编排领域行为
└────────────────────┘
         ↓
┌────────────────────┐
│   Domain Layer   │ ← 实体、值对象、领域服务、聚合
└────────────────────┘
         ↓
┌────────────────────┐
│ Infrastructure Layer│ ← 数据库、MQ、远程调用等技术细节
└────────────────────┘

实际项目结构:
com.mint.lottery
├── application
│   └── DrawAppService.java         ← 应用服务层
├── domain
│   ├── model
│   │   └── strategy/Strategy.java  ← 聚合根
│   └── service/DrawDomainService.java ← 领域服务层
├── infrastructure
│   └── repository/StrategyRepositoryImpl.java

它的关注点包括:

  • 协调:调用一个或多个领域服务/聚合完成某个“业务用例”(Use Case)
  • 转发:不做业务决策,把实际逻辑交给领域层处理
  • 事务控制:决定在什么范围内开启事务
  • 转换数据:将 Controller 的输入 DTO 映射为领域对象;将领域对象转换为响应结果

🌰举个例子:

@Service
public class DrawAppService {

    @Autowired
    private DrawDomainService drawDomainService;

    public DrawResultDTO doDraw(String userId, Long strategyId) {
        // 1. 调用领域服务执行业务逻辑
        DrawResult result = drawDomainService.executeDraw(userId, strategyId);

        // 2. 转换为 DTO 返回前端
        return new DrawResultDTO(result.getAwardId(), result.getAwardName());
    }
}

析这个服务干了:

  • 调用了领域服务 → 不负责业务细节
  • 提供给 controller 使用 → 属于 app 层
  • 不操作数据库 → 交给领域模型完成
  • 返回 DTO → 与前端交互

基础设施层(Infrastructure Layer)

显而易见基础设施层是为上层(应用层、领域层)提供技术支持的实现层,包括数据库访问、消息队列、缓存、文件存储、远程服务调用等。

它的关注点包括:

  • 数据持久化:Repository 实现(MyBatis、JPA、Mongo 等)
  • 消息通信:Kafka、RabbitMQ、RocketMQ
  • 缓存存取:Redis、Caffeine
  • 外部服务访问:HTTP、RPC(如 Feign、Dubbo)
  • 文件存储:OSS、MinIO
  • 框架集成:Spring、ShardingSphere、ElasticSearch 等

典型项目结构:

com.xxx.lottery
├── application
│   └── service/DrawAppService.java
├── domain
│   ├── model/strategy/Strategy.java
│   └── repository/IStrategyRepository.java
├── infrastructure
│   ├── repository/StrategyRepositoryImpl.java ← 实现 Repository 接口
│   └── mq/DrawEventProducer.java ← Kafka、RocketMQ 封装

领域事件(Domain Events)

领域事件是指领域中发生的、对业务有意义的事情,它是业务行为的结果,通常由实体、聚合在状态变化后发布。

简单来讲就是“某事已经发生”的一种陈述,例如:“用户注册成功了”、“订单已支付”、“抽奖完成”。🤖

为什么需要领域事件?🤔

现实世界中很多业务行为会引起后续连锁反应,而 DDD 鼓励我们:

  • 用事件解耦行为之间的耦合
  • 让聚合根只关注自身业务逻辑
  • 把“副作用”通过事件异步通知出去

工程设计

战略设计(Strategic Design) 解决“系统要如何拆分”的问题(宏观、架构视角)。

战术设计(Tactical Design) 解决“每个子系统内部要怎么建模”**的问题(微观、编码视角)。

战略设计主要解决如何从业务整体出发,划分出多个“子域”或限界上下文(Bounded Context):

  • 子域:按业务划分的大块区域,如“支付”、“订单”、“抽奖”、“库存”等
  • 限界上下文:系统中模型定义的一块边界,一个上下文只讲一种“语言”
  • 上下文映射:描述多个上下文之间的集成关系(如 ACL、防腐层、共享内核等)
  • 统一语言:在该上下文内,业务、开发都统一使用的术语集合
  • 关系模式:如合作伙伴关系、客户-供应商、共享内核、防腐层等

战术设计专注于在一个限界上下文中如何建模业务逻辑

  • 实体:有唯一标识、可变状态
  • 值对象:无标识、不可变
  • 聚合:一组实体 + 规则,入口是聚合根
  • 领域服务:逻辑不属于任何实体时的业务逻辑承载
  • 应用服务:协调用例执行(不含业务逻辑)
  • 领域事件:表示“某事已发生”的事件
  • 仓储:提供聚合的持久化操作接口
  • 工厂:封装复杂对象创建流程(不是 new)

领域模型概念

先介绍三个模型概念:

  • 领域模型(Domain Model) 是面向业务建模的方法,鼓励将行为和数据聚合在一个对象中
  • 充血模型是领域模型的实践体现(行为和数据放在一起)
  • 贫血模型是一种反面示例(数据和行为分离)

项目中贫血模型是怎么样的?

  • 常见于传统的 SpringMVC 项目,Controller → Service → Mapper,各层无行为封装。所有行为都集中在 Service,导致 Service 类巨肥。
  • 行为和数据分离,容易出现状态不一致(因为没有自治能力)

那充血模型呢?

  • 采用聚合建模 + 充血对象 + 领域服务辅助
    - 实体类(如 Strategy、Award) → 封装自己的行为
    - 聚合根(如 Strategy) → 管控所有子对象
    - 领域服务 → 封装横切逻辑或多个聚合协作
    
  • 聚合根有“身份”和“意志”,能够自己判断是否可执行某操作
  • 领域模型不再只是 DTO
  • 应用层只负责 orchestrate(编排),不会夹杂业务规则

****领域对象

贫血模型下的开发通常多个服务共用一个 DTO 对象,它只需要把对象所需要的属性传入即可。

但是在 DDD 的领域模型设计下,领域对象的设计是完全面向对象。

🌰一个直观的例子:在 DDD 中,“取消订单”不应由 OrderService.cancel(Order) 处理,而应该是 Order 自己决定能不能取消自己

public class Order {
    private OrderStatus status;
    private int itemCount;

    public void cancel() {
        if (this.status == OrderStatus.SHIPPED) {
            throw new IllegalStateException("不能取消已发货订单");
        }
        this.status = OrderStatus.CANCELED;
    }
}

实体

一个具有唯一标识(ID),并且其生命周期内状态会发生变化的对象。🤖

唯一标识例如用户 ID、订单 ID;状态可变可以理解为余额、状态、地址等可能改变。生命周期长意味着会持久化到数据库。

值对象

一个不需要 ID不可变、用来描述某个属性或概念的小对象,根据值来判断是否相等。🤖

聚合

聚合是一组相关实体和值对象的集合以一个“聚合根”实体作为入口,对外暴露统一访问方式。

🌰举个例子:

public class Strategy { // ← 聚合根
    private Long strategyId;
    private List<Award> awards; // 子实体
    private Stock stock;        // 值对象

    public Award draw() {
        if (stock.isEmpty()) throw new IllegalStateException("库存不足");
        return doDraw();
    }
}

仓储

仓储是一个抽象接口,用于封装对聚合根的持久化操作,让领域层不用关心数据存取的细节。🤖

在 domain 层义仓储接口:

public interface IUserRepository {
    void save(User user);
    User findById(Long id);
    boolean existsByEmail(String email);
}

实现仓储接口(在 infrastructure 层):

@Repository
public class UserRepositoryJpaImpl implements IUserRepository {
    @Autowired
    private UserJpaDao userJpaDao;

    @Override
    public void save(User user) {
        userJpaDao.save(user); // 实际用 JPA 保存
    }

    @Override
    public User findById(Long id) {
        return userJpaDao.findById(id).orElseThrow();
    }
}

适配器

适配器是实现“基础设施能力(infrastructure)”的一种组件,将外部系统的 API 适配为系统内部可以调用的形式。🤖

它通常实现某个接口,用于:

  • 调用远程服务(RPC、REST)
  • 发消息(MQ)
  • 操作数据库、缓存(有时也算)
  • 转换数据格式

在 domain 或 application 层定义接口:

public interface SmsService {
    void sendCode(String phone, String code);
}

实现适配器(infrastructure 层):

@Component
public class AliyunSmsAdapter implements SmsService {
    @Override
    public void sendCode(String phone, String code) {
        // 调用阿里云短信 SDK
        aliyunClient.sendSms(phone, code);
    }
}