第4章:领域驱动设计(DDD)在ERP中的应用
说实话,DDD这个概念在ERP圈子里被讨论了很多年。我最早接触它是在2015年,当时接手一个老旧的ERP重构项目。那代码写得,啧啧,一个订单Service里塞了三百行if-else。后来我硬着头皮用DDD的思路重新梳理了一遍,才算是把那个烂摊子给收拾了。
今天咱们就聊聊DDD在ERP里的核心玩法:限界上下文、聚合、实体与值对象。这些东西听起来玄乎,其实说白了就是一套让复杂业务变得可管理的思维工具。
4.1 限界上下文:ERP的边界划分
限界上下文是DDD里最基础的概念。我个人的理解是:它帮我们划清了业务模块的边界。
你想想看,一个ERP系统里,订单模块和库存模块都叫“商品”,但含义完全不同。订单里的商品关注的是价格、数量、折扣;库存里的商品关注的是批次、库位、保质期。如果把它们混在一起写代码,那维护成本会直线上升。
我在项目中遇到过这样的坑:一个团队把订单和库存的实体类放在同一个包里,结果每次改库存逻辑都要拉上订单组的人一起评审。后来我强制按限界上下文拆分,每个上下文有自己的数据库、自己的服务、自己的团队。效率一下子就上来了。
常见的ERP限界上下文划分如下:
| 限界上下文 | 核心职责 | 典型实体 |
|---|---|---|
| 订单上下文 | 管理销售订单、采购订单 | Order, OrderLineItem |
| 库存上下文 | 管理库存数量、库位、批次 | Inventory, Warehouse, Batch |
| 财务上下文 | 管理应收应付、总账 | Invoice, Payment, Account |
| 生产上下文 | 管理工单、BOM、工艺路线 | WorkOrder, BOM, Routing |
4.2 聚合:保证数据一致性的最小单元
聚合是DDD里最容易让人困惑的概念。我刚开始学的时候也绕了很久。后来我总结了一句话:聚合就是一组必须一起持久化的对象。
举个例子,一个订单包含多个订单行。如果订单行单独被修改,而订单总金额没更新,那数据就出问题了。所以订单和订单行应该属于同一个聚合,订单是聚合根。
我曾经在一个ERP项目里犯过错误:把订单行设计成独立的聚合,结果出现了订单总金额和明细金额对不上的情况。后来花了整整一周去修复数据。嗯,从那以后我再也不敢把聚合边界划得太小了。
聚合设计的关键规则:
- 聚合根是唯一的外部访问点:外部只能通过聚合根来操作聚合内的对象
- 聚合内强一致性:一个事务内完成所有修改
- 聚合间最终一致性:通过领域事件来同步
- 引用其他聚合时用ID:不要直接持有对象引用
来看一个简单的代码示例:
// 订单聚合根
public class Order {
private OrderId id;
private List<OrderLineItem> lineItems;
private Money totalAmount;
public void addLineItem(Product product, int quantity) {
// 业务规则:检查库存
// 业务规则:计算金额
OrderLineItem item = new OrderLineItem(product, quantity);
lineItems.add(item);
recalculateTotalAmount();
}
private void recalculateTotalAmount() {
this.totalAmount = lineItems.stream()
.map(OrderLineItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
// 订单行是聚合内的实体
public class OrderLineItem {
private ProductId productId;
private int quantity;
private Money unitPrice;
private Money subtotal;
public OrderLineItem(Product product, int quantity) {
this.productId = product.getId();
this.quantity = quantity;
this.unitPrice = product.getPrice();
this.subtotal = unitPrice.multiply(quantity);
}
}
4.3 实体与值对象:两种不同的建模思维
实体和值对象的区别,说白了就是:实体有身份,值对象只有值。
实体就像一个人,有身份证号,换了名字还是同一个人。值对象就像钱,100块就是100块,不管这张钞票的编号是多少。
在ERP里,这个区分特别重要。我举个例子:
- 实体:客户、订单、产品、仓库。它们有唯一标识,会经历状态变化。
- 值对象:金额、地址、日期范围、颜色。它们没有独立身份,只描述实体的某个属性。
我个人习惯把值对象设计成不可变的。为什么?因为值对象一旦创建就不应该被修改。你想啊,如果订单的金额可以被随意修改,那财务对账的时候不得疯掉?
值对象的设计要点:
- 不可变性:所有字段都是final,不提供setter
- 值相等性:重写equals和hashCode,基于所有字段比较
- 自包含行为:值对象可以有自己的业务方法,比如金额的加减乘除
- 无副作用:操作返回新对象,不修改自身
来看一个值对象的经典实现:
public final class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.amount = amount;
this.currency = 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);
}
public Money multiply(int quantity) {
return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0 && currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
}
4.4 领域事件:解耦聚合之间的通信
最后聊一下领域事件。这东西在ERP里太常用了。订单创建后要通知库存扣减,库存扣减后要通知财务记账。如果把这些逻辑都写在一个方法里,那代码就成了一团乱麻。
领域事件的玩法是:聚合根在状态变化时发布事件,其他上下文订阅并做出响应。这样每个上下文只关心自己的事,耦合度大大降低。
举个例子:
// 领域事件
public class OrderCreatedEvent {
private final OrderId orderId;
private final List<OrderLineItem> items;
private final Instant occurredOn;
public OrderCreatedEvent(OrderId orderId, List<OrderLineItem> items) {
this.orderId = orderId;
this.items = items;
this.occurredOn = Instant.now();
}
}
// 在订单聚合根中发布事件
public class Order {
private List<DomainEvent> events = new ArrayList<>();
public void create() {
// 业务逻辑...
events.add(new OrderCreatedEvent(this.id, this.lineItems));
}
public List<DomainEvent> getEvents() {
return Collections.unmodifiableList(events);
}
}
// 库存上下文订阅事件
public class InventoryEventHandler {
public void handle(OrderCreatedEvent event) {
for (OrderLineItem item : event.getItems()) {
inventoryService.reserve(item.getProductId(), item.getQuantity());
}
}
}
好了,关于DDD在ERP中的应用,今天就聊这么多。总结一下:
- 限界上下文帮我们划清业务边界
- 聚合保证数据一致性
- 实体和值对象帮我们正确建模
- 领域事件解耦上下文之间的通信
下一章咱们聊聊事件溯源和CQRS在ERP里的实战应用。到时候我会分享一个真实的库存对账案例,保证让你大开眼界。