第4章:领域驱动设计(DDD)在ERP中的应用

说实话,DDD这个概念在ERP圈子里被讨论了很多年。我最早接触它是在2015年,当时接手一个老旧的ERP重构项目。那代码写得,啧啧,一个订单Service里塞了三百行if-else。后来我硬着头皮用DDD的思路重新梳理了一遍,才算是把那个烂摊子给收拾了。

今天咱们就聊聊DDD在ERP里的核心玩法:限界上下文、聚合、实体与值对象。这些东西听起来玄乎,其实说白了就是一套让复杂业务变得可管理的思维工具。

4.1 限界上下文:ERP的边界划分

限界上下文是DDD里最基础的概念。我个人的理解是:它帮我们划清了业务模块的边界。

你想想看,一个ERP系统里,订单模块和库存模块都叫“商品”,但含义完全不同。订单里的商品关注的是价格、数量、折扣;库存里的商品关注的是批次、库位、保质期。如果把它们混在一起写代码,那维护成本会直线上升。

我在项目中遇到过这样的坑:一个团队把订单和库存的实体类放在同一个包里,结果每次改库存逻辑都要拉上订单组的人一起评审。后来我强制按限界上下文拆分,每个上下文有自己的数据库、自己的服务、自己的团队。效率一下子就上来了。

核心原则:每个限界上下文内部保持高内聚,上下文之间通过事件或API进行松耦合通信。

常见的ERP限界上下文划分如下:

限界上下文 核心职责 典型实体
订单上下文 管理销售订单、采购订单 Order, OrderLineItem
库存上下文 管理库存数量、库位、批次 Inventory, Warehouse, Batch
财务上下文 管理应收应付、总账 Invoice, Payment, Account
生产上下文 管理工单、BOM、工艺路线 WorkOrder, BOM, Routing
我的建议:刚开始做DDD时,不要追求完美的上下文划分。先按业务模块切一刀,运行一段时间后再调整。迭代比一步到位更重要。

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里,这个区分特别重要。我举个例子:

  • 实体:客户、订单、产品、仓库。它们有唯一标识,会经历状态变化。
  • 值对象:金额、地址、日期范围、颜色。它们没有独立身份,只描述实体的某个属性。

我个人习惯把值对象设计成不可变的。为什么?因为值对象一旦创建就不应该被修改。你想啊,如果订单的金额可以被随意修改,那财务对账的时候不得疯掉?

实战经验:在ERP里,金额、税率、折扣率这些都应该设计成值对象。我曾经见过有人把金额设计成double,结果出现了0.1+0.2不等于0.3的精度问题。后来我统一用Money值对象,内部用BigDecimal,再也没出过这种低级错误。

值对象的设计要点:

  1. 不可变性:所有字段都是final,不提供setter
  2. 值相等性:重写equals和hashCode,基于所有字段比较
  3. 自包含行为:值对象可以有自己的业务方法,比如金额的加减乘除
  4. 无副作用:操作返回新对象,不修改自身

来看一个值对象的经典实现:

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里太常用了。订单创建后要通知库存扣减,库存扣减后要通知财务记账。如果把这些逻辑都写在一个方法里,那代码就成了一团乱麻。

领域事件的玩法是:聚合根在状态变化时发布事件,其他上下文订阅并做出响应。这样每个上下文只关心自己的事,耦合度大大降低。

我的习惯:领域事件的命名用过去式,比如OrderCreated、InventoryAdjusted。这样读起来就像在说“已经发生的事”,语义更清晰。

举个例子:

// 领域事件
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里的实战应用。到时候我会分享一个真实的库存对账案例,保证让你大开眼界。