前言:超越 CRUD,拥抱真正的业务建模
在现代软件开发中,我们常常被 CRUD(创建、读取、更新、删除)操作所束缚。当我们面对一个新需求时,第一反应往往是:"数据库表怎么设计?"、"API 接口需要哪些字段?"。这种以数据为中心的思维方式,在简单场景下或许足够,但当业务复杂度增加时,代码很快就会变成难以维护的"意大利面条"。
领域驱动设计(Domain-Driven Design, DDD) 和 Clean Code 原则 提供了一套完整的解决方案,帮助我们将复杂的业务逻辑转化为清晰、可维护、可演进的代码结构。
一、反面教材:典型的贫血模型实现
让我们先看看一个常见的购物车实现:
java
// ❌ 反面示例:贫血模型
public class ShoppingCart {
private Long id;
private List<CartItem> items;
// getters and setters
}
public class CartItem {
private Long productId;
private String productName;
private BigDecimal price;
private Integer quantity;
// getters and setters
}
@Service
public class ShoppingCartService {
public void addItem(ShoppingCart cart, Long productId, Integer quantity) {
// 1. 检查商品是否存在
Product product = productRepository.findById(productId);
if (product == null) {
throw new IllegalArgumentException("商品不存在");
}
// 2. 检查库存
if (product.getStock() < quantity) {
throw new IllegalArgumentException("库存不足");
}
// 3. 检查是否已存在
CartItem existingItem = cart.getItems().stream()
.filter(item -> item.getProductId().equals(productId))
.findFirst()
.orElse(null);
if (existingItem != null) {
existingItem.setQuantity(existingItem.getQuantity() + quantity);
} else {
CartItem newItem = new CartItem();
newItem.setProductId(productId);
newItem.setProductName(product.getName());
newItem.setPrice(product.getPrice());
newItem.setQuantity(quantity);
cart.getItems().add(newItem);
}
}
public BigDecimal calculateTotal(ShoppingCart cart) {
return cart.getItems().stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
这个实现的问题在哪里?
-
贫血模型(Anemic Model)
ShoppingCart和CartItem仅仅是数据容器,没有任何行为。所有业务逻辑都挤在ShoppingCartService中。 -
业务规则泄露
"库存检查"、"重复商品合并"这些属于购物车自身的规则,却被放在了外部服务中。
-
对象状态不一致风险
通过 setter 直接修改数量,可能导致对象处于非法状态(例如数量为负数)。
-
难以测试和复用
想单独测试"购物车能否添加某个商品"?做不到!因为逻辑分散在服务层。
-
违反单一职责原则
ShoppingCartService既要处理商品添加,又要计算总价,还要处理促销,职责过于混杂。
二、正确姿势:基于 DDD 与 Clean Code 的重构
步骤 1:建立统一语言(Ubiquitous Language)
与领域专家(产品经理、运营人员)对话,提炼核心概念:
| 业务术语 | 代码模型 |
|---|---|
| 购物车(Shopping Cart) | ShoppingCart 聚合根 |
| 购物车项(Cart Item) | CartItem 值对象 |
| 商品(Product) | Product 实体 |
| 商品ID(Product ID) | ProductId 值对象 |
| 价格(Price) | Money 值对象 |
| 数量(Quantity) | Quantity 值对象 |
✅ DDD 核心思想:代码中的类名、方法名必须与业务语言一致,消除沟通歧义。
步骤 2:设计不可变的值对象(Value Object)
首先,将基础概念建模为值对象:
java
// ✅ 值对象:ProductId
@Value
@EqualsAndHashCode
public class ProductId {
private final Long value;
public ProductId(Long value) {
if (value == null || value <= 0) {
throw new IllegalArgumentException("商品ID必须大于0");
}
this.value = value;
}
@Override
public String toString() {
return value.toString();
}
}
// ✅ 值对象:Quantity
@Value
@EqualsAndHashCode
public class Quantity {
private final int value;
public Quantity(int value) {
if (value <= 0) {
throw new IllegalArgumentException("数量必须大于0");
}
this.value = value;
}
public Quantity add(Quantity other) {
return new Quantity(this.value + other.value);
}
public boolean isLessThan(Quantity other) {
return this.value < other.value;
}
}
// ✅ 值对象:Money
@Value
@EqualsAndHashCode
public class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负");
}
this.amount = amount;
this.currency = currency;
}
public Money multiply(int multiplier) {
return new Money(amount.multiply(BigDecimal.valueOf(multiplier)), currency);
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币类型不匹配");
}
return new Money(this.amount.add(other.amount), this.currency);
}
}
值对象的设计原则:
- 不可变性 :所有字段都是
final,创建后不可更改 - 完整性校验:构造函数中验证参数合法性
- 行为内聚 :提供相关操作方法(如
add,multiply) - 相等性基于值 :重写
equals和hashCode,基于字段值而非引用
步骤 3:构建充血的聚合根(Aggregate Root)
现在,让 ShoppingCart 成为一个真正的聚合根:
java
// ✅ 聚合根:ShoppingCart
public class ShoppingCart {
private final CartId id;
private final List<CartItem> items;
// 私有构造函数,强制通过静态工厂创建
private ShoppingCart(CartId id) {
this.id = id;
this.items = new ArrayList<>();
}
// 静态工厂方法:封装创建逻辑
public static ShoppingCart createNew() {
return new ShoppingCart(new CartId(UUID.randomUUID()));
}
// 核心业务行为:添加商品
public void addItem(Product product, Quantity quantity) {
// 1. 参数校验
if (product == null) {
throw new IllegalArgumentException("商品不能为空");
}
if (quantity == null) {
throw new IllegalArgumentException("数量不能为空");
}
// 2. 库存检查(委托给 Product)
if (!product.hasSufficientStock(quantity)) {
throw new IllegalArgumentException("库存不足");
}
// 3. 查找是否已存在
Optional<CartItem> existingItem = findItemByProductId(product.getId());
if (existingItem.isPresent()) {
// 更新现有项
CartItem updatedItem = existingItem.get().increaseQuantity(quantity);
// 替换列表中的项(保持不可变性)
int index = items.indexOf(existingItem.get());
items.set(index, updatedItem);
} else {
// 添加新项
items.add(CartItem.create(product, quantity));
}
}
// 核心业务行为:移除商品
public void removeItem(ProductId productId) {
items.removeIf(item -> item.getProductId().equals(productId));
}
// 核心业务行为:计算总价
public Money calculateTotal() {
return items.stream()
.map(CartItem::getTotalPrice)
.reduce(Money::add)
.orElse(new Money(BigDecimal.ZERO, "CNY"));
}
// 查询方法:获取所有项
public List<CartItem> getItems() {
// 返回不可变视图,防止外部修改
return Collections.unmodifiableList(items);
}
// 内部辅助方法
private Optional<CartItem> findItemByProductId(ProductId productId) {
return items.stream()
.filter(item -> item.getProductId().equals(productId))
.findFirst();
}
// 只读访问器
public CartId getId() {
return id;
}
}
购物车项(CartItem)作为值对象:
java
// ✅ 值对象:CartItem
@Value
@EqualsAndHashCode
public class CartItem {
private final ProductId productId;
private final String productName;
private final Money unitPrice;
private final Quantity quantity;
// 私有构造函数
private CartItem(ProductId productId, String productName, Money unitPrice, Quantity quantity) {
this.productId = productId;
this.productName = productName;
this.unitPrice = unitPrice;
this.quantity = quantity;
}
// 静态工厂方法
public static CartItem create(Product product, Quantity quantity) {
return new CartItem(
product.getId(),
product.getName(),
product.getPrice(),
quantity
);
}
// 行为方法:增加数量
public CartItem increaseQuantity(Quantity additionalQuantity) {
Quantity newQuantity = this.quantity.add(additionalQuantity);
return new CartItem(productId, productName, unitPrice, newQuantity);
}
// 计算总价
public Money getTotalPrice() {
return unitPrice.multiply(quantity.value());
}
}
步骤 4:完善 Product 实体
java
// ✅ 实体:Product
public class Product {
private final ProductId id;
private final String name;
private final Money price;
private final Quantity stock;
// 私有构造函数
private Product(ProductId id, String name, Money price, Quantity stock) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
}
// 静态工厂方法
public static Product create(ProductId id, String name, Money price, Quantity stock) {
return new Product(id, name, price, stock);
}
// 业务方法:检查库存是否充足
public boolean hasSufficientStock(Quantity requestedQuantity) {
return !requestedQuantity.isLessThan(stock);
}
// 只读访问器
public ProductId getId() { return id; }
public String getName() { return name; }
public Money getPrice() { return price; }
public Quantity getStock() { return stock; }
}
步骤 5:服务层的职责澄清
重构后的服务层变得极其简洁:
java
// ✅ 简洁的服务层
@Service
@Transactional
public class ShoppingCartApplicationService {
private final ProductRepository productRepository;
private final ShoppingCartRepository shoppingCartRepository;
public ShoppingCartApplicationService(
ProductRepository productRepository,
ShoppingCartRepository shoppingCartRepository) {
this.productRepository = productRepository;
this.shoppingCartRepository = shoppingCartRepository;
}
public void addItemToCart(String cartId, Long productId, int quantity) {
// 1. 获取领域对象
ShoppingCart cart = shoppingCartRepository.findById(new CartId(cartId));
Product product = productRepository.findById(new ProductId(productId));
Quantity qty = new Quantity(quantity);
// 2. 触发业务行为
cart.addItem(product, qty);
// 3. 保存变更
shoppingCartRepository.save(cart);
}
public Money getCartTotal(String cartId) {
ShoppingCart cart = shoppingCartRepository.findById(new CartId(cartId));
return cart.calculateTotal();
}
}
服务层的新角色:
- 协调者:获取领域对象,触发其行为
- 事务边界 :用
@Transactional保证操作原子性 - 防腐层:将外部请求(如 API 参数)转换为领域对象
三、设计原则深度解析
1. 不可变性(Immutability)的力量
值对象必须不可变
- 安全性 :避免意外修改。例如,如果
Money可变,外部代码可能修改购物车项的价格 - 线程安全:不可变对象天然线程安全
- 缓存友好:可以安全地缓存和共享
实体部分可变
- 身份标识不可变 :
CartId一旦创建就不能改变 - 业务状态可变 :
items列表可以修改,但只能通过行为方法
📌 经验法则:能用不可变就用不可变;必须可变时,严格控制变更入口。
2. 静态工厂方法 vs 构造函数
| 特性 | 构造函数 | 静态工厂方法 |
|---|---|---|
| 命名 | 固定(类名) | 自由(createNew, fromSnapshot) |
| 返回类型 | 必须是当前类 | 可返回子类或缓存实例 |
| 对象有效性 | 无法保证 | 可在校验后创建,确保有效 |
| 语义清晰度 | 较低 | 较高(Product.create(...) 比 new Product(...) 更清晰) |
在领域对象中,静态工厂方法是首选。
3. 充血模型:让对象拥有"智慧"
- 贫血模型:对象是" dumb data holder ",逻辑在服务层
- 充血模型:对象是" smart actor ",知道如何响应业务事件
充血模型的优势:
- 高内聚:相关数据和行为在一起
- 易于测试:可以单独测试对象的行为
- 业务表达力强:代码即文档
💡 Eric Evans(DDD 之父):"领域对象应该封装状态和行为,这是面向对象的核心。"
4. 聚合根的设计原则
- 一致性边界:聚合根负责维护内部对象的一致性
- 外部只能引用根 :其他对象只能持有
ShoppingCart的引用,不能直接持有CartItem - 事务一致性:对聚合的修改必须在单个事务中完成
在我们的例子中,ShoppingCart 是聚合根,CartItem 是内部值对象。
5. 防御性编程与 Fail-Fast
- 构造时校验:值对象在构造时验证参数合法性
- 方法参数校验:业务方法验证输入参数
- 早期失败:在问题发生时立即抛出异常,而不是让错误传播
这确保了对象始终处于有效状态。
四、Clean Code 原则的具体体现
1. 有意义的命名
addItem比add更清晰hasSufficientStock比checkStock更准确calculateTotal比getTotal更准确(因为涉及计算)
2. 函数只做一件事
每个方法都有单一职责:
addItem:处理商品添加逻辑calculateTotal:计算总价hasSufficientStock:检查库存
3. 抽象层次一致
同一方法中的代码应该在同一抽象层次:
java
// ✅ 好的抽象层次
public void addItem(Product product, Quantity quantity) {
validateInputs(product, quantity); // 高层抽象
checkInventory(product, quantity); // 高层抽象
updateCartItems(product, quantity); // 高层抽象
}
// ❌ 混合抽象层次
public void addItem(Product product, Quantity quantity) {
if (product == null) throw ... // 低层细节
if (!product.hasSufficientStock(quantity)) // 高层抽象
throw ...
// ... 混合细节和抽象
}
4. 避免副作用
- 查询方法无副作用 :
calculateTotal()、getItems()不修改对象状态 - 命令方法有明确意图 :
addItem()、removeItem()明确表示会修改状态
5. 使用描述性异常
- 抛出有意义的异常信息:"库存不足" 而不是 "Validation failed"
- 在合适的位置抛出异常:在业务规则违反的地方,而不是在调用方
五、如何将此模式应用到你的项目?
无论你的业务是金融、医疗、物流还是 IoT,只要涉及复杂状态变更 和多条件规则,都可以套用此模式:
1. 识别核心聚合根与值对象
- 聚合根:有唯一身份,是外部访问的入口点(如订单、用户、设备)
- 值对象:无身份,通过属性定义(如地址、价格、时间段、配置)
2. 将业务规则移入领域对象
问自己:"这个规则属于谁的职责?"
- "订单能否取消?" → 应该是
Order.canBeCancelled() - "用户密码是否符合复杂度要求?" → 应该是
Password.isValid()
3. 用行为方法替代 setter
- 禁止直接
setXXX - 提供
placeOrder(),cancel(),ship()等业务语义方法
4. 服务层只做协调
- 获取领域对象
- 触发其行为
- 处理事务和跨聚合操作
5. 逐步重构
如果你的代码已经是贫血模型,不要试图一次性重构:
- 先识别核心业务规则
- 将规则移到相关对象中
- 逐步移除服务层中的逻辑
- 最终形成充血模型
六、常见误区与最佳实践
误区 1:过度设计
问题 :为简单场景引入复杂的 DDD 模式
解决方案 :DDD 适用于复杂业务逻辑。对于简单的 CRUD,保持简单即可。
误区 2:聚合过大
问题 :将太多对象放入一个聚合,导致性能问题
解决方案:聚合应该尽可能小。只包含需要强一致性的对象。
误区 3:忽略 ORM 兼容性
问题 :JPA/Hibernate 需要无参构造函数和 setter
解决方案:
- 使用 Builder 模式
- 或接受在持久化层有轻微妥协,但领域层保持纯净
- 考虑使用支持不可变对象的 ORM(如 Spring Data JDBC)
误区 4:所有逻辑都放进聚合根
问题 :聚合根变得过于庞大
解决方案 :以下逻辑适合放在领域服务(Domain Service):
- 涉及多个聚合根的操作
- 无法自然归属到单一实体的复杂算法
- 需要外部依赖的业务逻辑(如调用第三方服务)
七、测试策略
充血模型极大地简化了测试:
单元测试购物车
java
@Test
void shouldAddNewItemWhenProductNotInCart() {
// Given
ShoppingCart cart = ShoppingCart.createNew();
Product product = Product.create(
new ProductId(1L),
"iPhone",
new Money(new BigDecimal("999.00"), "CNY"),
new Quantity(10)
);
Quantity quantity = new Quantity(2);
// When
cart.addItem(product, quantity);
// Then
assertThat(cart.getItems()).hasSize(1);
assertThat(cart.getItems().get(0).getQuantity()).isEqualTo(quantity);
assertThat(cart.calculateTotal()).isEqualTo(new Money(new BigDecimal("1998.00"), "CNY"));
}
@Test
void shouldThrowExceptionWhenInsufficientStock() {
// Given
ShoppingCart cart = ShoppingCart.createNew();
Product product = Product.create(
new ProductId(1L),
"iPhone",
new Money(new BigDecimal("999.00"), "CNY"),
new Quantity(1) // 只有1个库存
);
Quantity quantity = new Quantity(2); // 请求2个
// When & Then
assertThatThrownBy(() -> cart.addItem(product, quantity))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("库存不足");
}
测试优势:
- 无需 mock:纯内存操作,快速可靠
- 业务导向:测试用例直接反映业务场景
- 高覆盖率:容易覆盖各种边界条件