You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DDD中Category聚合根实现与自关联关系校验问题咨询

DDD Category聚合根自关联实现修正方案

现有实现的问题

  • 导航属性不全:仅声明了父分类ParentCategory,未维护当前分类的子分类集合,无法获取当前分类下的所有子分类完成重名校验
  • 方法命名错误:AddParentCategory的实际逻辑是为当前分类添加子分类,命名与逻辑相悖,极易引发后续业务逻辑错误
  • 边界异常未处理:未判断父分类为空的场景,访问ParentCategory.Name时会触发空引用异常
  • 校验逻辑缺失:现有校验仅判断了当前分类的父分类名称与新分类名是否重复,未遍历当前分类的子分类列表做重复判断

修正后实现代码

public class Category : AggregateRoot
{
    // 维护子分类私有集合,对外仅暴露只读视图,保证聚合内数据一致性
    private readonly List<Category> _subCategories = new();
    public IReadOnlyList<Category> SubCategories => _subCategories.AsReadOnly();

    // 支持创建无父级的顶级分类
    public static Result<Category> Create(string name, Category parentCategory = null)
    {
        var result = new Result<Category>();
        var nameResult = Name.Create(name);
        result.WithErrors(nameResult.Errors);
        
        if (result.IsFailed) 
            return result;

        var category = new Category(nameResult.Value, parentCategory);
        result.WithValue(category);
        return result;
    }

    private Category() : base() { }

    private Category(Name name, Category parentCategory) : this()
    {
        Name = name;
        ParentCategory = parentCategory;
        ParentId = parentCategory?.Id;
        // 自动同步父分类的子集合
        parentCategory?._subCategories.Add(this);
    }

    public Name Name { get; private set; }
    public virtual Category ParentCategory { get; private set; }
    // 冗余存储父分类ID,适配持久化查询需求
    public int? ParentId { get; private set; }

    // 修正方法命名,符合添加子分类的实际逻辑
    public Result<Category> AddSubCategory(string subCategoryName)
    {
        var result = new Result<Category>();
        // 校验同父级下子分类名称是否重复
        var isDuplicate = _subCategories.Any(c => 
            c.Name.Value.Equals(subCategoryName, StringComparison.OrdinalIgnoreCase));
        
        if (isDuplicate)
        {
            var errorMessage = string.Format(Validations.Repetitive, DataDictionary.CategoryName);
            result.WithError(errorMessage);
            return result;
        }

        var subCategoryResult = Create(subCategoryName, this);
        return subCategoryResult.IsFailed ? subCategoryResult.ToResult() : subCategoryResult;
    }

    // 可选扩展:修改父分类方法,内置循环引用校验
    public Result ChangeParent(Category newParent)
    {
        var result = new Result();
        // 校验避免循环引用
        if (newParent != null && IsDescendantOf(newParent, this))
        {
            result.WithError("不能将子分类设置为当前分类的父级,避免循环关联");
            return result;
        }
        // 同步原有父分类的子集合
        ParentCategory?._subCategories.Remove(this);
        // 更新父级关联
        ParentCategory = newParent;
        ParentId = newParent?.Id;
        // 同步新父分类的子集合
        newParent?._subCategories.Add(this);
        
        return result;
    }

    // 辅助方法:判断目标分类是否是当前分类的后代
    private bool IsDescendantOf(Category target, Category ancestor)
    {
        var current = target;
        while (current.ParentCategory != null)
        {
            if (current.ParentCategory == ancestor) 
                return true;
            current = current.ParentCategory;
        }
        return false;
    }
}

补充说明

  • 同父级下子分类不重名的校验逻辑放在聚合内即可,符合DDD聚合内业务规则封装原则
  • 若需要全局分类名唯一,需要通过领域服务调用ICategoryRepository查询全量分类数据完成校验,单个聚合根无法持有全局数据
  • 若使用EF Core等ORM框架做持久化,需要单独配置自关联外键:将ParentId作为外键关联到Category的主键Id,同时配置ParentCategory和_subCategories的父子映射关系

内容的提问来源于stack exchange,提问作者MrJahanbin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 17:54:04