2、库存领域模型设计:DDD领域驱动设计入门、库存核心聚合、领域事件与限界上下文
好,咱们进入正题。
库存系统,说白了就是管「货」的。但货怎么管?管哪些东西?这就涉及到领域模型设计了。我见过不少团队,一上来就建表、写CRUD,结果业务一复杂,代码就成了一团乱麻。嗯,今天我们就用DDD的思路,把库存的根儿捋清楚。
2.1 DDD领域驱动设计入门
DDD,全称是Domain-Driven Design。很多人一听就觉得高大上,其实没那么玄乎。它的核心思想就一句话:让代码模型跟业务模型对齐。
你想想看,业务上说的是「入库」、「出库」、「盘点」,代码里却只有「insert」、「update」、「select」,这中间就有鸿沟。DDD就是填这个沟的。
核心概念速览:
- 实体(Entity):有唯一标识,会变化。比如一个商品,它的库存数量会变,但商品ID不变。
- 值对象(Value Object):没有ID,只描述属性。比如「地址」、「金额」。
- 聚合(Aggregate):一组相关对象的集合,有根。比如「订单」聚合了「订单项」。
- 领域事件(Domain Event):业务中发生的重要事情。比如「库存不足」就是一个事件。
- 限界上下文(Bounded Context):划清边界,每个上下文有自己的语言。
我个人习惯,在开始写代码之前,先跟业务方聊透。聊什么?聊他们口中的「库存」到底指什么。是仓库里的实物?还是系统里的记录?还是可售库存?这三个东西,在DDD里可能就是三个不同的聚合。
2.2 库存核心聚合
库存系统里,最核心的聚合有三个:商品、仓库、库存记录。我一个个说。
2.2.1 商品聚合
商品,是库存的「标的物」。它本身不存数量,但它是所有库存操作的依据。
// 商品聚合根
public class Product : AggregateRoot
{
public string ProductId { get; private set; }
public string Sku { get; private set; }
public string Name { get; private set; }
public string CategoryId { get; private set; }
// 商品规格(值对象)
public ProductSpecification Spec { get; private set; }
// 领域方法
public void ChangeCategory(string newCategoryId)
{
// 这里可以加业务规则
CategoryId = newCategoryId;
AddDomainEvent(new ProductCategoryChangedEvent(this));
}
}
注意,商品聚合里不包含库存数量。为什么?因为数量是跟仓库绑定的。同一个商品,在不同仓库里数量不同。你把它放商品里,就乱套了。
我的经验: 曾经有个项目,把库存数量直接挂在商品表上。结果一做多仓,代码改得想哭。后来拆成独立的库存记录聚合,世界清净了。
2.2.2 仓库聚合
仓库,是库存的「容器」。它定义了库存的物理位置和属性。
// 仓库聚合根
public class Warehouse : AggregateRoot
{
public string WarehouseId { get; private set; }
public string Code { get; private set; }
public string Name { get; private set; }
public Address Location { get; private set; } // 值对象
public WarehouseType Type { get; private set; } // 枚举:实体仓、虚拟仓
public bool IsActive { get; private set; }
public void Deactivate()
{
if (!IsActive) return;
IsActive = false;
AddDomainEvent(new WarehouseDeactivatedEvent(this));
}
}
仓库本身不存商品数量,它只描述「我在哪儿、我是什么类型」。真正的数量,在库存记录里。
2.2.3 库存记录聚合
这才是库存系统的「心脏」。库存记录,是商品+仓库+数量的组合。
// 库存记录聚合根
public class InventoryRecord : AggregateRoot
{
public string RecordId { get; private set; }
public string ProductId { get; private set; }
public string WarehouseId { get; private set; }
public int QuantityOnHand { get; private set; } // 实物库存
public int QuantityAvailable { get; private set; } // 可售库存
public int QuantityReserved { get; private set; } // 预占库存
// 核心领域方法
public void Reserve(int quantity)
{
if (quantity <= 0)
throw new ArgumentException("数量必须大于0");
if (QuantityAvailable < quantity)
{
AddDomainEvent(new InventoryShortageEvent(this, quantity));
throw new InsufficientInventoryException("库存不足");
}
QuantityAvailable -= quantity;
QuantityReserved += quantity;
AddDomainEvent(new InventoryReservedEvent(this, quantity));
}
public void ReleaseReservation(int quantity)
{
// 释放预占的逻辑
}
}
注意: 库存操作一定要用领域方法,不要直接set属性。为什么?因为每次操作都可能触发业务规则和领域事件。直接set,这些逻辑就丢了。
2.3 领域事件设计
领域事件,是DDD里很妙的一个东西。它让聚合之间可以「松耦合」地通信。
举个例子:当库存不足时,系统需要通知采购部门。如果直接在库存记录里调用采购接口,那就耦合了。正确做法是:抛出领域事件,让其他服务去订阅。
// 领域事件基类
public abstract class DomainEvent
{
public DateTime OccurredOn { get; } = DateTime.UtcNow;
public string EventId { get; } = Guid.NewGuid().ToString();
}
// 具体事件
public class InventoryShortageEvent : DomainEvent
{
public string ProductId { get; }
public string WarehouseId { get; }
public int ShortageQuantity { get; }
public InventoryShortageEvent(InventoryRecord record, int shortageQty)
{
ProductId = record.ProductId;
WarehouseId = record.WarehouseId;
ShortageQuantity = shortageQty;
}
}
public class InventoryReservedEvent : DomainEvent
{
public string ProductId { get; }
public string WarehouseId { get; }
public int ReservedQuantity { get; }
public InventoryReservedEvent(InventoryRecord record, int qty)
{
ProductId = record.ProductId;
WarehouseId = record.WarehouseId;
ReservedQuantity = qty;
}
}
我建议,领域事件命名用「过去时」。比如「InventoryReservedEvent」,表示「库存已经被预占了」。这样语义更清晰。
领域事件的作用:
- 解耦聚合之间的直接调用
- 记录业务变更的审计日志
- 触发跨限界上下文的流程(比如通知采购、更新缓存)
2.4 限界上下文划分
限界上下文,是DDD里最容易被忽略、但最重要的概念。说白了,就是划地盘。
在库存系统里,我一般会划分出这几个上下文:
| 限界上下文 | 核心职责 | 典型聚合 |
|---|---|---|
| 库存核心 | 管理实物库存、可售库存 | InventoryRecord, Product, Warehouse |
| 库存预留 | 处理订单预占、释放 | Reservation, Allocation |
| 库存盘点 | 实物盘点、差异调整 | Stocktake, Adjustment |
| 库存预警 | 监控库存水位、触发补货 | AlertRule, ReplenishmentRequest |
每个上下文都有自己的「通用语言」。比如在「库存核心」里,「可用库存」指的是实物减去预占。但在「库存预警」里,「可用库存」可能还要考虑在途库存。同一个词,不同上下文含义不同,这很正常。
避坑指南: 我曾经在一个项目里,试图用一个「库存服务」搞定所有事情。结果服务越来越臃肿,改一个盘点逻辑,影响到了订单预占。后来拆成四个限界上下文,每个独立部署,问题迎刃而解。
2.5 知识体系总览
下面这张图,是我梳理的库存领域模型核心结构。你可以看到,聚合、事件、上下文之间的关系。
你看,每个限界上下文都有自己的聚合。它们通过领域事件通信,互不干扰。这就是DDD的魅力所在。
总结一下:
- DDD的核心是让代码模型对齐业务模型
- 库存核心聚合:商品、仓库、库存记录
- 领域事件用过去时命名,解耦聚合间通信
- 限界上下文划清边界,每个上下文有自己的语言
嗯,这一章的内容就到这儿。记住,模型设计不是一蹴而就的。我每次做新项目,都会跟业务方反复对几轮,直到大家说的「库存」是同一个东西。这个功夫,值得花。
公众号:蓝海资料掘金营,微信deep3321