DDD 中的空实体

Null entities in DDD

Product 实体或聚合为例,它引用了另一个名为 Category 的实体。在像 CategorizeProduct 这样的领域事件发生之前,类别的状态是空的。为了在 C# 中表示,我必须使用 NULL。我不能在 C# 中使用 Nullable<T>,因为它只能用于值类型。

在 DDD 中使用 NULL 是反模式吗?建议的一些解决方案是 (1) 使用 MayBe<T> (2) 使用 collection 代替单个实体。

MayBe<t> 是一个选项,但是在 DDD 中是否有不同的方法来处理空实体状态?

我理解您的担忧 - NULL 可能意味着尚未加载该类别,或者可能意味着尚未分配任何类别。

一个选项是添加一个可为 null 的 属性 CategoryId,其中包含分配的类别的 ID。如果分配了类别,则始终填写 ID,否则为空。这意味着如果填写了CategoryID而Category为null,则表示分配了一个类别但没有加载。

如果您使用 Entity Framework 加载或保留您的域,这种方法自然适合。

 public class Product
 {
      public Product()
      {
          // validate entity 
      }

      public int? CategoryId { get; private set; }
      public Category Category { get; private set; } 
 }

对于这些类型的事情,您可以使用 null object pattern。想想例如String.Empty 属性 作为一个简单的例子。

如果您的 null 对象的某些方法应该有不同的行为,您可以将其设为私有内部 class,它派生自 Category 并覆盖特定行为。

为了你的Category

public class Category
{
    private class UnassignedCategory : Category
    {
        public UnassignedCategory() : base("") { }
    }

    private static readonly UnassignedCategory _unassigned = new UnassignedCategory();

    public static Category Unassigned 
    { 
        get { return _unassigned; }
    }

    public Category(string name) 
    {
        this.Name = name;
    }

    public string Name { get; private set; }

    public bool IsAssigned 
    {
        get { return ReferenceEquals(this, _unassigned); }
    }
}

请注意,在上面的示例中,由于 Category 上的默认无参数构造函数不存在,并且无法更改 Name 属性 的值,因此subclass 并不是真正需要的,我们可以简单地使用静态 Category 实例。

用法:

public class Product
{
    public Product()
    {
        Category = Category.Unassigned;
        // other stuff
    }

    // other stuff.

    public void AssignCategory(Category category) 
    {
        // Any associated logic.
        Category = category;
    }
    public Category Category { get; private set; } 
}

如果您要从数据库中保存和加载 null(即未分配的)类别,您可能希望使用此主题的变体,为 [= 使用明确的(不可变的)id/attribute 43=] 与其他 Category 属性一起保存并用于 IsAssigned 检查。

可行的替代方案

一个完全可行的替代方案是继续做您现在正在做的事情,使用 null 参考。如果需要,您可以添加一些扩展方法,让您的生活更轻松。

public static MyCategoryExtensions
{
    public static bool IsNullOrEmpty(this Category category) 
    {
        return category == null || string.IsNullOrEmpty(category.Name);
    }
    public static Category EmptyIfNull(this Category category) 
    {
        return category ?? new Category("");
    }
    public static Category DefaultIfNullOrEmpty(this Category category) 
    {
        return category.IsNullOrEmpty() 
            ? new Category(Category.DefaultName)
            : category;
    }
}