DDD聚合设计:以不变式为指导 -CodeOpinion


您如何构成一个DDD聚合?对我而言,聚合设计涉及对不变性的理解。不变是必须始终保持一致的业务规则。了解不变式将指导您的聚合设计。聚合是基于不变性和一致性定义边界的另一个示例。
 
送货案例
我将使用的示例是“Shipment”的概念。您可以将其视为送餐服务。您有在餐馆拿到的食物,然后送到家中。送货有2个停靠点/站点:在餐厅取货,以及在你家交货。
站点的重要方面是:它们必须经历状态转换。站点的初始状态为”运输中In Transit“,一旦送货员到达餐厅取走起食物,站点就会进入“到达Arrived”状态。一旦送货员带着食物离开餐厅,并在前往您家的路上,这时站点处于“已出发Departed”状态。
 
不变量
我们的货件中有几个不变式:

  1. 站点必须按照上面概述的确切顺序进行其状态转换。
  2. 只有离开取餐点,才能处于送货到你家过程中。

第一个不变式很容易控制,因为我们可以在站点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站点已经完成其完整的状态进度。
 
聚合设计:不变式
使用不变量作为设计聚合的指南。不变是必须始终保持一致的业务规则。如果您有不变量且未使用聚合,建议您对其进行建模以亲自尝试。您将对状态如何发生变化的了解会降低难度,因为所有交互都必须经过聚合根。只有单向状态可以更改。