章
目
录
我们常常会听到领域驱动设计(DDD,Domain-Driven Design )这个术语,但很多人对它的理解还停留在表面,网络上的相关文章大多晦涩难懂。今天,咱们就深入浅出地聊聊DDD,看看它究竟是什么,以及如何在实际项目中应用。
一、DDD究竟是什么?
简单来说,DDD是一种构建复杂系统的软件开发方法,它的核心在于将代码结构与业务领域的实际需求紧密结合。和传统开发对着需求文档写代码不同,DDD强调拉着业务方一起绘制领域模型,让代码能够精准地反映业务本质。用大白话讲,就是用代码还原业务的真实情况,而不只是单纯实现功能。
二、从用户注册案例看传统开发模式
为了让大家更好地理解,我们以一个简单的用户注册功能为例。在这个案例中,业务规则规定用户名必须唯一,密码要满足复杂度要求,注册成功后还得记录日志。
在传统开发模式下,可能会写出这样的代码:
@Controller
public class UserController {
public void register(String username, String password) {
// 校验密码
// 检查用户名
// 保存数据库
// 记录日志
// 所有逻辑混在一起
}
}
有些开发者可能会说,代码肯定不能这么写,得进行分层,比如分成controller、service、dao层。于是,代码就变成了这样:
// Service层:仅有流程控制,业务规则散落在各处
public class UserService {
public void register(User user) {
// 校验规则1:写在工具类里
ValidationUtil.checkPassword(user.getPassword());
// 校验规则2:通过注解实现
if (userRepository.exists(user)) { ... }
// 数据直接传递到DAO
userDao.save(user);
}
}
虽然代码进行了分层,流程看起来也清晰了不少,但这并不意味着就是DDD。在这段代码里,User对象只是用来承载数据的“贫血模型”,业务逻辑被分散到了各个外部模块。
三、DDD的正确打开方式——以用户注册为例
在DDD中,一些业务逻辑会被内聚到领域对象中。还是以用户注册为例,密码规则的校验就可以放到User对象里。用专业的话来说,就是业务规则被封装在领域对象内部,对象不再仅仅是“数据袋子” ,而是“充血模型”。具体代码如下:
// 领域实体:业务逻辑内聚
public class User {
public User(String username, String password) {
// 密码规则内聚到构造函数
if (!isValidPassword(password)) {
throw new InvalidPasswordException();
}
this.username = username;
this.password = encrypt(password);
}
// 密码复杂度校验是实体的职责
private boolean isValidPassword(String password) { ... }
}
从这段代码可以看出,校验密码的逻辑下沉到了User领域实体对象中,这就是DDD和传统开发的一个重要区别。
四、DDD的关键设计要素
(一)聚合根(Aggregate Root)
在实际业务场景中,比如用户(User)和收货地址(Address)有关联的情况。按照传统方式,会在Service中分别管理User和Address。而在DDD里,会将User作为聚合根,由它来控制Address的增删操作。代码示例如下:
public class User {
private List<Address> addresses;
// 添加地址的逻辑由聚合根控制
public void addAddress(Address address) {
if (addresses.size() >= 5) {
throw new AddressLimitExceededException();
}
addresses.add(address);
}
}
通过这种方式,业务逻辑更加集中,管理也更加方便。
(二)领域服务与应用服务
- 领域服务:主要处理跨多个实体的业务逻辑。比如说转账操作,涉及到两个账户,这种核心业务逻辑就由领域服务来处理。
- 应用服务:负责协调流程,比如调用领域服务,再加上发送消息等操作。具体代码示例如下:
// 领域服务:处理核心业务逻辑
public class TransferService {
public void transfer(Account from, Account to, Money amount) {
from.debit(amount); // 账户扣款逻辑内聚在Account实体
to.credit(amount);
}
}
// 应用服务:编排流程,不包含业务规则
public class BankingAppService {
public void executeTransfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId);
Account to = accountRepository.findById(toId);
transferService.transfer(from, to, new Money(amount));
messageQueue.send(new TransferEvent(...)); // 基础设施操作
}
}
从代码中可以看出,领域服务专注于业务逻辑的实现,应用服务则负责流程的编排。
(三)领域事件(Domain Events)
领域事件是用事件来明确表达业务的变化。比如用户注册成功后,会触发UserRegisteredEvent事件。代码示例如下:
public class User {
public void register() {
// ...注册逻辑
this.events.add(new UserRegisteredEvent(this.id)); // 记录领域事件
}
}
这样,当业务发生变化时,可以通过这些事件进行后续的处理。
五、传统开发与DDD的差异对比
下面我们通过表格来直观地对比一下传统开发和DDD的区别:
维度 | 传统开发 | DDD |
---|---|---|
业务逻辑归属 | 分散在Service、Util、Controller等多个地方 | 内聚在领域实体或领域服务中 |
模型作用 | 主要作为数据载体,即贫血模型 | 是携带行为的业务模型,也就是充血模型 |
技术实现影响 | 数据库表结构驱动代码设计 | 业务需求驱动数据库表结构设计 |
六、电商下单案例:DDD的实际应用
为了进一步加深大家对DDD的理解,我们再来看一个电商下单的案例。假设业务需求是用户下单时要校验库存、使用优惠券、计算实付金额并生成订单。
(一)传统写法(贫血模型)
// Service层:大杂烩式下单
public class OrderService {
@Autowired private InventoryDAO inventoryDAO;
@Autowired private CouponDAO couponDAO;
public Order createOrder(Long userId, List<ItemDTO> items, Long couponId) {
// 1. 校验库存(散落在Service)
for (ItemDTO item : items) {
Integer stock = inventoryDAO.getStock(item.getSkuId());
if (item.getQuantity() > stock) {
throw new RuntimeException("库存不足");
}
}
// 2. 计算总价
BigDecimal total = items.stream()
.map(i -> i.getPrice().multiply(i.getQuantity()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 3. 应用优惠券(规则写在工具类)
if (couponId != null) {
Coupon coupon = couponDAO.getById(couponId);
total = CouponUtil.applyCoupon(coupon, total); // 优惠逻辑隐藏在Util
}
// 4. 保存订单(纯数据操作)
Order order = new Order();
order.setUserId(userId);
order.setTotalAmount(total);
orderDAO.save(order);
return order;
}
}
这种传统写法存在一些问题,比如库存校验、优惠计算等逻辑分散在Service、Util、DAO中,Order对象只是数据载体,当需求变更时,修改代码就像“考古”一样困难。
(二)DDD写法(充血模型)
// 聚合根:Order(承载核心逻辑)
public class Order {
private List<OrderItem> items;
private Coupon coupon;
private Money totalAmount;
// 构造函数内聚业务逻辑
public Order(User user, List<OrderItem> items, Coupon coupon) {
// 1. 校验库存(领域规则内聚)
items.forEach(item -> item.checkStock());
// 2. 计算总价(业务逻辑在值对象)
this.totalAmount = items.stream()
.map(OrderItem::subtotal)
.reduce(Money.ZERO, Money::add);
// 3. 应用优惠券(规则在实体内部)
if (coupon != null) {
validateCoupon(coupon, user); // 优惠券使用规则内聚
this.totalAmount = coupon.applyDiscount(this.totalAmount);
}
}
// 优惠券校验逻辑(业务归属清晰)
private void validateCoupon(Coupon coupon, User user) {
if (!coupon.isValid() || !coupon.isApplicable(user)) {
throw new InvalidCouponException();
}
}
}
// 领域服务:协调下单流程
public class OrderService {
public Order createOrder(User user, List<Item> items, Coupon coupon) {
Order order = new Order(user, convertItems(items), coupon);
orderRepository.save(order);
domainEventPublisher.publish(new OrderCreatedEvent(order)); // 领域事件
return order;
}
}
采用DDD写法后,库存校验封装在了OrderItem值对象中,优惠券规则内聚在Order实体内部方法里,计算逻辑由Money值对象保证精度。当业务发生变化时,只需要修改领域对象即可。比如产品提出新需求:优惠券需满足“订单满100减20”,且仅限新用户使用。传统开发方式需要修改Service层和Util类,而DDD只需要修改Order.validateCoupon()方法。
七、DDD的适用场景
DDD虽然强大,但并非适用于所有场景。在业务复杂的系统,像电商、金融、ERP系统,以及需求频繁变更的互联网业务中,DDD能够发挥出巨大的优势。但对于简单的CRUD操作,比如管理后台、数据报表这类功能,使用DDD反而会增加开发成本,有些小题大做。
可以这样判断是否适合使用DDD:当修改业务规则时,只需要调整领域层代码,而不需要改动Controller或DAO,那就说明DDD在这个项目中得到了较好的落地。
总之,希望通过本文,大家对DDD有了更清晰的认识。如果在阅读过程中有任何疑问,欢迎在评论区留言讨论。