如何为在一个上下文中而不是在另一个上下文中的聚合根的实体编写命令?

Jos*_*off 4 domain-driven-design command-pattern cqrs

我正在为一家公司开发一个项目,该公司找到供应商为员工搬迁提供服务.这些服务的东西,搬运工没有专门做,如准备钢琴或过境或建造板条箱贵重物品.

在此域中,订单具有1:多个位置.

在移动行业中,订单经常不断变化,直到供应商执行他所要求的服务为止.因此,在我们的模型中,我们有一些适用于订单和位置的状态(例如,已提交,已取消,已暂停).

这里有一些非常简单的业务规则.这是一个抽样:

  1. 当订单处于"暂停"状态时,所有位置都将处于"暂停"状态.
  2. 如果其父订单处于暂停状态,则无法取消暂停.

等等.根据这些规则,我觉得这形成了一个聚合根边界.因此,我有一个MyClient.Statuses.Order聚合,其中Statuses是上下文/服务的名称/您想要调用它的任何内容:

public class Order {
    private Guid _id;
    private OrderStatus _status;

    public void PlaceOnHold() {
        if (_status == OrderStatus.Cancelled)
            // throw exception

        _status = OrderStatus.OnHold;
        Locations.ForEach(loc => loc.PlaceOnHold());
    } 

    public void PlaceLocationOnHold(Guid id) {
        if (_status == OrderStatus.Cancelled)
            // throw exception
        Locations.Single(loc => loc.Id == id).PlaceOnHold();
    }

    // etc...

        private Location[] Locations;
}

internal class Location { 
    public Guid Id;
    public LocationStatus Status;

    public void PlaceOnHold() {
        // It's ok for a cancelled location on a non-cancelled order,
        // but a Location cannot be placed On Hold if it's Cancelled so 
        // just ignore it
        if (Status == LocationStatus.Cancelled)
            return; 

        Status = LocationStatus.OnHold;
    }
}
Run Code Online (Sandbox Code Playgroud)

这两个对象(Order,Location)在其他上下文中都有GUID id(例如,对于没有状态转换的基于CRUD的属性).所以现在我们终于得到了我的问题:

如何编写命令和处理程序以保持位置?

我想保持这个DRY和面向服务的东西,以尽量减少耦合,但很难在一个地方保持两个实体之间的父子关系.

选项1 - 单个位置ID:

public class PlaceLocationOnHold_V1 {
    public readonly Guid Id;
}

public class PlaceLocationOnHold_V1Handler {
    public void Handle(PlaceLocationOnHold_V1 command) {
        // This is typically a no-no.  Should only fetch by OrderId:
        var aggregate = _repository.GetByLocationId(command.Id);  

        aggregate.PlaceLocationOnHold(command.Id);
        _repository.Save();
    }
}
Run Code Online (Sandbox Code Playgroud)

选项2 - 订单ID和位置ID:

public class PlaceLocationOnHold_V2 {
    public readonly Guid OrderId; // This feels redundant
    public readonly Guid LocationId;
}

public class PlaceLocationOnHold_V2Handler {
    public void Handle(PlaceLocationOnHold_V2 command) {
        var aggregate = _repository.GetById(command.OrderId);
        aggregate.PlaceLocationOnHold(command.LocationId);
        _repository.Save();
    }
}
Run Code Online (Sandbox Code Playgroud)

选项3 - 带有类的单个参数,封装了"一个位置,属于一个订单"

public class LocationIdentity {
    public Guid Id;
    public Guid OrderId;
}

public class PlaceLocationOnHold_V3 {
    public readonly LocationIdentity Location;
}

public class PlaceLocationOnHold_V3Handler {
    public void Handle(PlaceLocationOnHold_V3 command) {
        var aggregate = _repository.GetById(command.Location.OrderId);  
        aggregate.PlaceLocationOnHold(command.Location.Id);
        _repository.Save();
    }
}
Run Code Online (Sandbox Code Playgroud)

Dan*_*Dan 8

查看有效聚合设计的Vaughn Vernon文章.具体来说,第2部分 - 有一些关于建模聚合的良好信息,这些聚合相互通信.

您的设计中缺少的要点是,这些都是您已经提到的AR - 它们是全局可识别的.所以他们应该通过ID引用彼此.订单不应包含子集合.

因此,您的Order类将包含LocationIds集合,您的Location将具有OrderId.

public class Order
{
    private Guid _id;
    private OrderStatus _status;
    private Guid[] _locationIds;
    //...
}

public class Location
{
    private Guid _id;
    private Guid _orderId;
    //...
}
Run Code Online (Sandbox Code Playgroud)

一旦你正确地解决了这个问题,选项#1就有意义了.由于Location本身就是一个AR,您可以实例化它并直接在其上调用PlaceOnHold,而无需通过Order AR.

至于一个AR的变化流入其他AR的情况(即保持顺序也使得所有位置也被搁置),您可以使用域事件或最终一致性.

public class Order
{
    //... private instance variables

    public void PlaceOnHold()
    {
        if (_status == OrderStatus.Cancelled)
          // throw exception

        _status == Orderstatus.OnHold;

        DomainEvents.Handle(new OrderPlacedOnHold(_id)); // handle this, look up the related locations and call PlaceOnHold on each of them)
    }
}
Run Code Online (Sandbox Code Playgroud)

对于您可能尝试删除某个位置的保留但是该命令处于暂停状态以使该操作非法的情况,您可以在命令处理程序中实例化Order对象并将其传递到Location的RemoveFromHold方法.弗农提到了这一点并重申了这一点,仅仅因为每个事务只能改变一个AR并不意味着你不能在事务中实例化多个AR.

public class RemoveHoldFromLocation : IHandler<RemoveHoldFromLocationCommand>
{
    public void Execute(RemoveHoldFromLocationCommand cmd)
    {
        var location = locationRepo.Get(cmd.LocationId);
        var order = orderRepo.Get(location.GetOrderId());

        location.RemoveHold(order.GetStatus());
    }
}

public class Location
{
    //... private instance variables, etc.

    public void RemoveHold(OrderStatus orderStatus)
    {
        if (orderStatus == OrderStatus.OnHold)
            // throw Exception

        _status == LocationStatus.OnHold;
    }
}
Run Code Online (Sandbox Code Playgroud)

这只是伪代码所以原谅错别字等.类似的代码示例在Vernon PDF中.