【问题标题】:Enforce business rules out of Aggregate从聚合中强制执行业务规则
【发布时间】:2021-12-22 21:30:46
【问题描述】:

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

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

有问题的规则:

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

为了执行这些规则,我尝试将餐厅存储库接口传递给菜单(我更关注 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;
    }

    ...
}

【问题讨论】:

    标签: c# .net domain-driven-design cqrs clean-architecture


    【解决方案1】:

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

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

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

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

    现在激活菜单的工作交给 MenuChangedEvent 处理程序 - 经常使用流行的 MediatR 库来连接它。

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

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

    我们不要忘记 to 和/或委托 - 如果您使用关系数据库,那么为什么不实现复合键,在这种情况下,对于由 RestaurantIdName 组成的 Menu - 那么不变性也将被强制由分贝。
    获取唯一的约束异常 - 未保存聚合,未发送事件等,您只需要在某处处理它。

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

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

    【讨论】:

    • 感谢您冗长而全面的回答。 1. 基于 EventStore 的方法。要激活 Menu,Menu 必须满足其他业务规则:例如,必须至少有一组(Inactive Menu 可以为空)。 EventStore 是基于事务的方法,对吗?那么如果我们在 Menu 中破坏规则,它会恢复 Restaurant 中的 ActiveMenuId 更改吗? 2. 域服务。所以试图改变菜单中的内部名称,我必须参考域服务。在那种情况下 ChangeInternalName 方法可以有 Internal 访问修饰符?
    • 1 - 菜单应该负责它自己的状态正确性 - 我会说它应该验证它自己至少有一个组 - 通过公共方法添加和删除组,这将验证这一点。创建菜单聚合也强制此规则,不会有问题(在构造函数中强制至少一组)。 2-至于名称更改-在持久化之前和/或在持久化时使用域服务将其公开并在另一个步骤中强制唯一性。如果有问题,聚合将不会被保存,您将处理错误。
    • 所以上面的流程将被放入一些 cqrs 命令处理程序中,例如CreateMenuCommand。通过在持久化时进行验证,我的意思是如果使用了来自答案的复合键,您可以处理数据库异常。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-01
    • 1970-01-01
    相关资源
    最近更新 更多