从聚合中执行业务规则

Enforce business rules out of Aggregate

目前我正在学习 DDD 和 CQRS 方法。我开始了一个与餐厅领域相关的项目。

我发现(使用事件风暴)的模块之一是菜单。在Menus模块中,餐厅经理可以存储多个Menus。菜单是 AggregateRoot,它在菜单中的组和连接项(位置)之间执行规则。

有问题的规则:

  1. 餐厅中的一个菜单可以同时激活。
  2. 菜单的内部名称在餐厅内必须是唯一的。

为了执行该规则,我尝试将 Restaurant 存储库界面传递给菜单(我更关注 1. 规则,因为 2. 更难)。我觉得我做错了什么。我有一个使用域服务来强制执行的想法,经过分析我认为它不会更好。你对我有什么建议吗?谢谢:)

public class Menu : AggregateRoot<MenuId>
{
    public string InternalName { get; private set; }
    
    public RestaurantId RestaurantId { get; private set; }

    public IReadOnlyList<Group> Groups => _groups;
    private List<Group> _groups = new();

    ...

    internal void Activate()
    {
        CheckRule(new ActiveMenuMustHaveAtLeastOneGroup(_groups));
        
        _groups.ForEach(x => x.CheckConsistency());
    }

    public void ChangeInternalName(string newInternalName, IRestaurantRepository 
         restaurantRepository)
    {
        CheckRule(new InternalNameMustBeUniqueInRestaurantMenusRule(RestaurantId, 
             newInternalName, restaurantRepository));
        InternalName = newInternalName;
    }
}

public class Restaurant : AggregateRoot<RestaurantId>
{
    public MenuId ActiveMenuId { get; private set; }

    public IReadOnlyList<MenuId> MenuIds => _menuIds;
    private List<MenuId> _menuIds = new();

    ...

    public void ChangeActiveMenu(MenuId newActiveMenuId, IMenuRepository menuRepository)
    {
        CheckMenuExists(newActiveMenuId);
        CheckRule(new CannotActivateActiveMenuRule(ActiveMenuId, newActiveMenuId));
        
        var menu = menuRepository.GetAsync(newActiveMenuId).Result;
        menu.Activate();
        ActiveMenuId = newActiveMenuId;
    }

    ...
}

好的,让我们尝试从头开始处理它 - 请注意,我的回答是>基于意见<,大多数设计决策也是如此。

第一个问题:
分配活动菜单是 Restaurant 的工作,因此这个聚合应该与 Menu 保持耦合——假设它存储它的 ID。只有一个 ID。
选择要激活的新菜单时,Restaurant AR 可以轻松检查它是否为当前 ID,无需存储库。
现在,如果给定的菜单 id 不同,那么 Restaurant 应该有办法通知 Menu 上下文有关更改的信息 - 这是一件好事,菜单知道它是否处于活动状态 - 这完全是另一回事。

因此,带有事件处理程序的 CQRS 出现了 - Restaurant 应该发出一个 MenuChanged 事件,而这个事件 处理程序 应该负责更新 menu合计。
因此,您应该在 ChangeActiveMenu 方法中启动域事件,该方法将在聚合持久化时启动(通常由存储库执行持久化)。

    public void ChangeActiveMenu(MenuId newActiveMenuId)
    {
        CheckRule(new CannotActivateActiveMenuRule(ActiveMenuId, newActiveMenuId));
        ActiveMenuId = newActiveMenuId;
        this.EventStore.Add(new MenuChangedEvent(newActiveMenuId));
    }

现在激活菜单的工作转到了 MenuChangedEvent 处理程序 - 连接它通常是使用流行的 MediatR 库完成的。

第二个问题: 可以通过多种方式检查聚合根中名称的唯一性。
有一件事是肯定的——聚合不应该强制不变量超出它的范围。因此,在这种情况下,不变量是 Name 并且跨越边界将让 Menu 检查多个聚合实例的一致性。

首先,您可以实施一个域服务,该服务将获取存储库并检查唯一性。或者你甚至可以创建这样的服务作为应用层服务。

我们不要忘记和/或委托 - 如果您使用关系数据库,那么为什么不实现一个复合键,在这种情况下 MenuRestaurantIdName 组成- 然后数据库也会强制不变性。
获取唯一约束异常 - 不保存聚合,不发送事件等,您只需要在某个地方处理它。

这将是我的 goto 解决方案,除此之外只是扩展 Restaurant 聚合以包含 Menu 作为其中的一部分。

同样,这是基于意见的,有很多很多方法可以解决这个问题。