聚合根外执行业务规则:DDD餐厅领域菜单模块规则实现咨询
问题解决方案
规则1:同餐厅同一时间仅一份Menu处于激活状态
你现有代码中的Restaurant聚合根本身就持有ActiveMenuId属性,该规则天然属于Restaurant的职责边界,无需将仓储传入Menu实体,调整逻辑如下:
- 移除聚合根对仓储的依赖:Menu的
Activate方法仅校验自身内部规则(如必须包含至少一个分组、分组数据一致性),Restaurant的ChangeActiveMenu方法仅校验聚合内部规则(如待激活菜单属于当前餐厅、未处于激活状态) - 跨聚合逻辑放到应用层编排:在调用
ChangeActiveMenu前,先通过Menu仓储取出待激活的Menu实例,调用Activate完成自身校验,再将MenuId传入Restaurant的ChangeActiveMenu方法完成激活状态替换 - 并发兜底:在数据库层面给菜单表添加
(restaurant_id, is_active)联合唯一索引,未激活菜单的is_active字段设为null(null不参与唯一索引校验),从存储层面保证不会出现多份同时激活的菜单
规则2:Menu内部名称同餐厅唯一
该类跨实体的集合校验有三种成熟的落地方式,可根据业务场景选择:
- 数据库唯一索引兜底:直接给菜单表添加
(restaurant_id, internal_name)联合唯一索引,出现重复插入/更新时捕获数据库异常转换为业务错误返回,实现成本最低,适合并发度不高的内部管理场景 - 应用层前置校验:在创建菜单、修改菜单名称的业务逻辑执行前,先调用Menu仓储的
ExistsByRestaurantIdAndInternalName方法做唯一性校验,通过后再执行后续操作,配合数据库索引兜底可覆盖绝大多数场景 - 领域服务封装复用:如果多段业务逻辑都需要用到名称唯一性校验,可以将校验逻辑封装到
MenuUniquenessCheckDomainService中,由应用层调用该服务完成校验,不要将领域服务传入Menu实体内部,避免实体和外部服务耦合
调整后的代码示例
领域层代码
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) { // 仅做自身属性基础校验,不依赖外部资源 CheckRule(new InternalNameCannotBeEmptyRule(newInternalName)); CheckRule(new InternalNameLengthValidRule(newInternalName)); 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) { CheckRule(new MenuBelongsToRestaurantRule(_menuIds, newActiveMenuId)); CheckRule(new CannotActivateActiveMenuRule(ActiveMenuId, newActiveMenuId)); ActiveMenuId = newActiveMenuId; } }
应用层代码示例
public class RestaurantMenuAppService { private readonly IRestaurantRepository _restaurantRepository; private readonly IMenuRepository _menuRepository; private readonly IUnitOfWork _unitOfWork; // 构造注入省略 public async Task ChangeActiveMenuAsync(RestaurantId restaurantId, MenuId menuId, CancellationToken ct) { var restaurant = await _restaurantRepository.GetByIdAsync(restaurantId, ct); if (restaurant == null) throw new BusinessException("餐厅不存在"); var menu = await _menuRepository.GetByIdAsync(menuId, ct); if (menu == null) throw new BusinessException("菜单不存在"); // 执行菜单自身激活校验 menu.Activate(); // 执行餐厅激活菜单变更 restaurant.ChangeActiveMenu(menuId); await _unitOfWork.SaveChangesAsync(ct); } public async Task ChangeMenuInternalNameAsync(MenuId menuId, string newInternalName, CancellationToken ct) { var menu = await _menuRepository.GetByIdAsync(menuId, ct); if (menu == null) throw new BusinessException("菜单不存在"); // 前置校验名称唯一性 var nameExists = await _menuRepository.ExistsByRestaurantIdAndNameAsync( menu.RestaurantId, newInternalName, excludeMenuId: menuId, cancellationToken: ct); if (nameExists) throw new BusinessException("同餐厅下已存在相同名称的菜单"); menu.ChangeInternalName(newInternalName); await _unitOfWork.SaveChangesAsync(ct); } }
内容的提问来源于stack exchange,提问作者mr0406
相关产品推荐
相关产品推荐

