从聚合中执行业务规则
Enforce business rules out of Aggregate
目前我正在学习 DDD 和 CQRS 方法。我开始了一个与餐厅领域相关的项目。
我发现(使用事件风暴)的模块之一是菜单。在Menus模块中,餐厅经理可以存储多个Menus。菜单是 AggregateRoot,它在菜单中的组和连接项(位置)之间执行规则。
有问题的规则:
- 餐厅中的一个菜单可以同时激活。
- 菜单的内部名称在餐厅内必须是唯一的。
为了执行该规则,我尝试将 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
检查多个聚合实例的一致性。
首先,您可以实施一个域服务,该服务将获取存储库并检查唯一性。或者你甚至可以创建这样的服务作为应用层服务。
我们不要忘记和/或委托 - 如果您使用关系数据库,那么为什么不实现一个复合键,在这种情况下 Menu
由 RestaurantId
和 Name
组成- 然后数据库也会强制不变性。
获取唯一约束异常 - 不保存聚合,不发送事件等,您只需要在某个地方处理它。
这将是我的 goto 解决方案,除此之外只是扩展 Restaurant
聚合以包含 Menu
作为其中的一部分。
同样,这是基于意见的,有很多很多方法可以解决这个问题。
目前我正在学习 DDD 和 CQRS 方法。我开始了一个与餐厅领域相关的项目。
我发现(使用事件风暴)的模块之一是菜单。在Menus模块中,餐厅经理可以存储多个Menus。菜单是 AggregateRoot,它在菜单中的组和连接项(位置)之间执行规则。
有问题的规则:
- 餐厅中的一个菜单可以同时激活。
- 菜单的内部名称在餐厅内必须是唯一的。
为了执行该规则,我尝试将 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
检查多个聚合实例的一致性。
首先,您可以实施一个域服务,该服务将获取存储库并检查唯一性。或者你甚至可以创建这样的服务作为应用层服务。
我们不要忘记和/或委托 - 如果您使用关系数据库,那么为什么不实现一个复合键,在这种情况下 Menu
由 RestaurantId
和 Name
组成- 然后数据库也会强制不变性。
获取唯一约束异常 - 不保存聚合,不发送事件等,您只需要在某个地方处理它。
这将是我的 goto 解决方案,除此之外只是扩展 Restaurant
聚合以包含 Menu
作为其中的一部分。
同样,这是基于意见的,有很多很多方法可以解决这个问题。