从贫血领域到领域驱动

3
我是一个能够翻译文本的助手。
我尝试寻找一个清晰简单的示例来解释“贫血领域”的真正含义。虽然有很多相关的理论和答案,但我仍然无法完全理解“贫血领域”所代表的含义。因此,我认为最好的方法是看一个虚拟的实际例子,了解什么是贫血领域设计,然后再考虑如何将其发展为面向领域的设计...
假设我们有一个类型为 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”。

如果我将这个逻辑写在一个单独的服务类中(我称之为“引擎”),我们有:

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如何构建这样的类的实例?有什么替代方案吗?


1
考虑领域模型时,我的第一个建议是忘记持久性,它会使事情变得混乱。如果您有一段跨实体的逻辑,则答案可能是更高级别的聚合根或领域级“服务”。 - Adam Houldsworth
因此,仅与当前实例相关的逻辑需要放在实体中,但依赖于外部上下文的逻辑需要保持不变。对吗? - Lucian
在这种情况下,似乎逻辑与此实体的子实体相关,因此应该由此实体管理。父实体对其子实体负责(不幸地;-) - Adam Houldsworth
1个回答

6
我试图找到一个清晰简单的例子来说明贫血领域模型的真正含义。
实际上,从贫血领域模型转换为丰富领域模型非常容易。
1. 将所有属性设置器设置为“private”,然后添加方法以更改模型状态。 2. 评估所有迪米特法则违规情况,并在适当的位置添加方法。
最终,您将拥有一个正确的模型。
在您的情况下,我建议将该逻辑封装在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集合(只将其作为成员字段)。但是很难说,因为我不知道在其他情况下该类的使用方式。
为什么使用私有setters
每次您必须更改多个属性时,使用方法来描述行为通常更好,因为这样就不可能忘记更改所有必需的属性。方法还更好地描述了您要做什么,而不是更改一组属性。
即使只更改一个属性,该属性也可能将类设置为无效状态,因为更改可能与类中的其他信息不兼容。不要忘记,封装是OOP的核心原则之一。

第一点似乎陈述得有些奇怪。公共属性设置器通常是更改模型状态的适当方式。我同意你应该从私有开始,直到你有充分的理由将它们变为公共的。 - Ben Aaronson
@BenAaronson:这是为了挑战开发者。每当你需要更改多个属性时,用方法来描述行为通常比直接更改属性更好,因为这样就不会忘记更改所有必需的属性。方法还可以更好地描述你要做什么,而不是只更改一组属性。 - jgauffin
如果情况是一个属性可以被更改而且模型仍然有效,那么几乎没有区别。唯一有意义的区别就是遵守规定。你可以改变一个属性Surname =“ Houldsworth”,也可以使用方法如ChangeSurname(“ Houldsworth”)来改变它。在功能上没有任何区别,表现差异在于你要求模型根据请求更改其状态,因此责任似乎是模型的。在良好的领域模型中,表达性比实现优先级更高。 - Adam Houldsworth
@AdamHouldsworth 好的,不过我认为表达上的差异取决于你的背景。由于 C# 中属性的普及,自然而然地会认为每次访问属性都是要求类执行其 get 或 set 方法。 - Ben Aaronson
1
@BenAaronson 是的,但属性的另一个假设是后备代码轻量级,并且领域实体的假设是它们管理有效性 - 这两个方面都是实现详细信息和实体责任。实现详细信息可能会在将来发生变化,因此编码对它们的依赖是一种风险。这就是主要差异是纪律的论点所在之处。尽管像所有事物一样,有时贫血模型是完全良好的设计选择,而不是所有东西都需要100%真实的DDD方法。 - Adam Houldsworth
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接