您如何构成一个DDD聚合?对我而言,聚合设计涉及对不变性的理解。不变是必须始终保持一致的业务规则。了解不变式将指导您的聚合设计。聚合是基于不变性和一致性定义边界的另一个示例。
送货案例
我将使用的示例是“Shipment”的概念。您可以将其视为送餐服务。您有在餐馆拿到的食物,然后送到家中。送货有2个停靠点/站点:在餐厅取货,以及在你家交货。
站点的重要方面是:它们必须经历状态转换。站点的初始状态为”运输中In Transit“,一旦送货员到达餐厅取走起食物,站点就会进入“到达Arrived”状态。一旦送货员带着食物离开餐厅,并在前往您家的路上,这时站点处于“已出发Departed”状态。
不变量
我们的货件中有几个不变式:
- 站点必须按照上面概述的确切顺序进行其状态转换。
- 只有离开取餐点,才能处于送货到你家过程中。
第一个不变式很容易控制,因为我们可以在站点Stop本身中包含该逻辑:
public abstract class Stop { public int StopId { get; set; } public StopStatus Status { get; set; } public int Sequence { get; set; }
public void Arrive() { if (Status != StopStatus.InTransit) { throw new InvalidOperationException("Stop has already arrived."); }
Status = StopStatus.Arrived; }
public void Depart() { if (Status == StopStatus.Departed) { throw new InvalidOperationException("Stop has already departed."); }
if (Status == StopStatus.InTransit) { throw new InvalidOperationException("Stop hasn't arrived yet."); }
Status = StopStatus.Departed; } }
|
第二个不变条件要困难一些:交货只有等到提货完成后。
问题是单个站点不了解其他站点。表示取件停止对象无法访问送达停止对象。这就是聚合的来源。
聚合
聚合是形成一致性边界的域对象的集合。
我们送货Shipment的聚合包括:取货站点和交付站点。Shipment 就是所谓的聚合根。
聚合根是聚合中所有交互的网关。换句话说,您仅公开聚合根。调用代码无法直接访问其中站点Stop。因此,您可以对整个聚合强制执行不变式:
public class Shipment { private readonly IList<Stop> _stops;
public Shipment(IList<Stop> stops) { _stops = stops; }
public void Arrive(int stopId) { var currentStop = _stops.SingleOrDefault(x => x.StopId == stopId); if (currentStop == null) { throw new InvalidOperationException("Stop does not exist."); }
var previousStopsAreNotDeparted = _stops.Any(x => x.Sequence < currentStop.Sequence && x.Status != StopStatus.Departed); if (previousStopsAreNotDeparted) { throw new InvalidOperationException("Previous stops have not departed."); }
currentStop.Arrive(); }
public void Pickup(int stopId) { var currentStop = _stops.SingleOrDefault(x => x.StopId == stopId); if (currentStop == null) { throw new InvalidOperationException("Stop does not exist."); }
if (currentStop is PickupStop == false) { throw new InvalidOperationException("Stop is not a pickup."); }
currentStop.Depart(); }
public void Deliver(int stopId) { var currentStop = _stops.SingleOrDefault(x => x.StopId == stopId); if (currentStop == null) { throw new InvalidOperationException("Stop does not exist."); }
if (currentStop is DeliveryStop == false) { throw new InvalidOperationException("Stop is not a delivery."); }
currentStop.Depart(); }
public bool IsComplete() { return _stops.All(x => x.Status == StopStatus.Departed); } }
|
在Arrive()方法中,我们确认已经离开先前所有站点。这将强制站点以正确的顺序完成。
这意味着在我们可以开始交货Delivery的状态之前,取货Pickup站点已经完成其完整的状态进度。
聚合设计:不变式
使用不变量作为设计聚合的指南。不变是必须始终保持一致的业务规则。如果您有不变量且未使用聚合,建议您对其进行建模以亲自尝试。您将对状态如何发生变化的了解会降低难度,因为所有交互都必须经过聚合根。只有单向状态可以更改。