第 1 章:DDD 入门
学习目标
- 理解 DDD 解决的问题与传统开发的差异
- 掌握 DDD 的核心概念:领域、子域、限界上下文
- 学会用统一语言沟通业务
- 识别 DDD 落地的常见坑
一、为什么需要 DDD?
传统开发最大的问题是业务与技术脱节:产品说"下单",代码里却是 OrderInfoDao.insert(),同名不同义,改需求到处翻代码。
java
// 传统贫血模型:业务散落在 Service 里,实体只是数据载体
public class Order {
private Long id;
private String status; // 状态:已创建/已支付/已发货...
private BigDecimal amount;
// 没有业务方法,只有 getter/setter
}
@Service
public class OrderService {
public void pay(Long orderId) {
Order order = orderDao.findById(orderId).get();
if (!"CREATED".equals(order.getStatus())) {
throw new RuntimeException("订单状态不对");
}
order.setStatus("PAID");
orderDao.save(order);
// 状态判断、规则校验散落各处
}
}⚠️ 坑 1:贫血模型把所有逻辑塞进 Service,后期 Service 膨胀成"上帝类",几千行无法维护。
二、DDD 的核心思想
DDD(Domain-Driven Design,领域驱动设计)强调业务复杂度是软件的核心,代码必须直接表达业务。两大支柱:
- 统一语言(Ubiquitous Language):开发、产品、测试用同一套词汇
- 模型驱动设计:代码结构 = 业务结构
java
// DDD 充血模型:实体自带业务逻辑
public class Order {
private OrderId id;
private OrderStatus status; // 枚举替代魔法字符串
private Money amount;
public void pay() {
if (status != OrderStatus.CREATED) {
throw new OrderStatusException("订单状态不对:" + status);
}
this.status = OrderStatus.PAID;
// 状态变更的规则封装在实体内部,外部只调用
}
}⚠️ 坑 2:
status用String还是Enum?必须用枚举。魔术字符串无法编译期检查,会出现"PAID"和"paid"共存的灵异 bug。
三、领域与子域
把整个业务范围叫"领域",按重要性拆成子域:
text
电商领域
├── 核心子域(核心竞争力)
│ ├── 商品(定价、库存)
│ └── 订单(下单、支付)
├── 支撑子域(必要但不独特)
│ └── 物流(发货、配送)
└── 通用子域(谁都能用)
└── 认证、通知java
// 子域在代码里体现为独立的 Bounded Context(限界上下文)
com.example.ecommerce
├── catalog/ // 商品上下文
│ ├── domain/
│ ├── application/
│ └── infrastructure/
├── order/ // 订单上下文
│ ├── domain/
│ ├── application/
│ └── infrastructure/
└── delivery/ // 物流上下文⚠️ 坑 3:把"通用子域"当核心子域自己造轮子,典型反例:自己实现一套 OAuth2。直接用成熟方案(Authorization Server、Keycloak)省一个月。
四、限界上下文(Bounded Context)
每个子域独立成一个限界上下文,上下文内部模型一致,跨上下文靠**防腐层(ACL)**翻译。
java
// 订单上下文里的用户(只关心 ID 和昵称)
public class Buyer {
private final UserId userId;
private final String nickname;
}
// 客服上下文里的用户(关心联系方式、订单历史)
public class Customer {
private final UserId userId;
private final String phone;
private final List<OrderId> recentOrders;
}
// 防腐层:跨上下文调用时翻译
@Component
public class CustomerTranslator {
public Buyer toBuyer(Customer customer) {
return new Buyer(customer.getUserId(), customer.getNickname());
}
}⚠️ 坑 4:跨上下文直接传数据库实体,导致两边改了字段互相影响。必须转换,一次转换写 10 行代码,换来后期不踩雷。
五、统一语言落地
把会议室讨论的术语,变成代码里的类名、字段名、枚举名:
java
// 业务术语:商品 SKU、SKU 组合、套装价
public class Sku {
private final SkuId skuId;
private final BigDecimal price;
private final Stock stock;
}
public class SkuCombination { // 套装 = 多个 SKU 的组合
private final List<SkuId> skuIds;
private final Money bundlePrice;
}会议里大家说"套装",代码里就是 SkuCombination,没有"组合商品"和"套装"两个名字并存。
六、DDD 不是什么
- ❌ DDD 不是银弹,CRUD 报表类业务用 DDD 反而过度设计
- ❌ DDD 不是架构,整洁架构/六边形/洋葱是 DDD 落地时搭配的架构
- ❌ DDD 不是必须微服务,单体里照样能用 DDD
⚠️ 坑 5:一上来就拆 5 个微服务,DDD 战术设计还没熟悉。先单体 + DDD,业务跑通再拆。
本章小结
| 要点 | 关键 |
|---|---|
| 贫血模型 | 只有数据没有业务,逻辑全在 Service |
| 充血模型 | 实体自带业务方法,状态变更受控 |
| 统一语言 | 业务术语 = 类名/字段名 |
| 领域/子域 | 核心、支撑、通用三类 |
| 限界上下文 | 业务边界,内部模型一致 |
| 防腐层 | 跨上下文转换,防止模型泄漏 |
动手练习
- 领域画布:挑一个你熟悉的业务(食堂打饭、图书馆借书),列出 3 个核心子域和 5 个术语
- 充血改造:把上面
OrderService.pay改写为Order.pay(),并加上OrderStatus枚举 - 上下文划分:把电商系统拆成"商品/订单/支付/物流"四个包,每个包独立
domain/application/infrastructure目录
下一章:第 2 章:战略设计 →