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 里
🌰举个例子:我们不能把“支付”放到 User 或 Order 里,因为:
- 支付行为不属于用户的职责
- 它需要协调订单、账户、支付渠道等多个对象
应用服务(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);
}
}