【问题标题】:From anemic domain to domain driven从贫血领域到领域驱动
【发布时间】:2014-07-24 22:49:49
【问题描述】:

我试图找到一个简单明了的例子来说明贫血域的真正含义。周围有很多理论,也有很多很好回答的问题。尽管如此,我仍然无法清楚地了解“贫血领域”的含义到底在多大程度上。因此,我相信看到一个虚弱的领域设计的虚拟实际示例会更简单,而不是问你如何将其演变为领域驱动的设计......

所以,假设我们有一个 TaskData 类型的数据实体:

public class TaskData
{
    public Guid InternalId { get; set; }

    public string Title { get; set; }

    public string Details { get; set; }

    public TaskState ExplicitState { get; set; }

    public IEnumerable<TaskData> InnerTasks { get; set; }

}

并且需要一个名为“ActualState”的附加属性,这是一个计算状态:如果任务有内部子任务,则值严格取决于子任务,否则, “ActualState”等于“ExplicitState

如果我在单独的 service 类(我称它们为“engines”)中编写此逻辑,我们有:

internal class TaskStateCalculator
{
    public TaskState GetState(TaskData taskData)
    {
        if (taskData.InnerTasks.Any())
        {
            if (taskData.InnerTasks.All(x => this.GetState(x) == TaskState.Done))
            {
                return TaskState.Done;
            }
            if (taskData.InnerTasks.Any(x => this.GetState(x) == TaskState.InProgress))
            {
                return TaskState.InProgress;
            }

            return TaskState.Default;
        }

        return taskData.ExplicitState;
    }       
}

第一个问题是:

即使 TaskStateCalculator 服务/引擎是我的域层的一部分,上述代码是否反映了贫乏的域设计? 如果是,为了避免这种情况,我们需要移动 TaskData 类中的逻辑(并将 TaskData 重命名为 Task)。我说的对吗?

第二个问题是(实际上是一连串):

如果我们遇到更困难的情况怎么办?假设在 Task 实体中需要一个名为 ComputeSomething 的属性,并且该属性的逻辑需要访问整个 Task 的 repository。在这种情况下,Task 类将依赖于TaskRepository。这样可以吗? EF 如何构造此类的实例?有什么选择?

【问题讨论】:

  • 在考虑域模型时,我的第一条建议是忘记持久性,它会搅浑水。如果您有一段跨越实体的逻辑,答案可能是更高级别的聚合根或域级别的“服务”。
  • 因此,仅与当前实例相关的逻辑需要放入实体中,但依赖于外部上下文的逻辑需要保留。是吗?
  • 在这种情况下,看起来逻辑与该实体的子实体有关,因此也应由该实体管理。父母要对自己的孩子负责(不幸的是;-)

标签: c# entity-framework design-patterns anemic-domain-model


【解决方案1】:

我试图找到一个简单明了的例子来说明贫血域的真正含义

事实上,从贫乏的领域模型转变为丰富的领域模型真的很容易。

  1. 将所有属性设置器设置为private,然后如果要更改模型的状态,请添加方法。
  2. 评估所有Law of Demeter 违规并在合适的地方添加方法。

最终你会得到一个正确的模型。

在您的情况下,我会将该逻辑封装在 TaskData 中,因为您的 TaskStateCalculator 违反了得墨忒耳法则

public class TaskData
{
    public Guid InternalId { get; private set; }

    public string Title { get; private set; }

    public string Details { get; private set; }

    public TaskState ExplicitState { get; private set; }

    public IEnumerable<TaskData> InnerTasks { get; private set; }

    public TaskState GetState()
    {
        if (!InnerTasks.Any())
            return ExplicitState;

        if (InnerTasks.All(x => this.GetState(x) == TaskState.Done))
        {
            return TaskState.Done;
        }

        if (InnerTasks.Any(x => this.GetState(x) == TaskState.InProgress))
        {
            return TaskState.InProgress;
        }

        return TaskState.Default;
    }       
}

另一件事是我可能根本不会向外界公开 InnerTasks 集合(只是将其作为成员字段)。但是很难说,因为我不知道该类在其他场景中是如何使用的。

为什么是私有设置器

每次您必须更改多个属性时,通常最好使用方法来描述行为,因为这样就不可能忘记更改所有必需的属性。与更改一组属性相比,方法还可以更好地描述您尝试执行的操作。

即使您只是更改了一个属性,该属性也可以将类设置为无效状态,因为更改可能与类中的其余信息不兼容。别忘了封装是OOP的核心原则之一

【讨论】:

  • 为什么还要在方法GetState 上使用taskData 参数?您将方法的结果基于参数和当前 (this) 对象。
  • 第 1 点似乎是一种奇怪的表述方式。公共属性设置器通常是更改模型状态的适当方式。我同意您应该先将它们设为私有,直到您有充分的理由将它们公开为止,但
  • @BenAaronson:挑战开发者。每次您必须更改多个属性时,通常最好用一种方法来描述行为,因为这样就不可能忘记更改所有必需的属性。与更改一组属性相比,方法还可以更好地描述您尝试执行的操作。
  • 即使您只是更改单个属性,该属性也可以将类设置为无效状态,因为更改可能与类中的其余信息不兼容。不要忘记封装是 OOP 的核心原则之一。
  • @BenAaronson 是的,但另一个关于属性的假设是支持代码是轻量级的,而域实体的假设是它们管理有效性——这两者都是实现细节和实体的责任.实现细节将来可能会发生变化,因此对它们的依赖进行编码是一种风险。这就是主要区别在于纪律的论据。尽管与所有事物一样,有时贫血模型是非常好的设计选择,但并非所有事物都需要 100% 真正的 DDD 方法。