DDD:技术上是实体,但看起来像值对象?

DDD: Entity technically, but looks like value object?

首先让我描述一个点位域。我们有一个网站,客户可以在那里下订单。要下订单,客户必须提供一些数据。这个过程分为几个步骤。在每个步骤中,客户端仅提供部分数据。当客户完成最后一步时 - 订单所需的所有数据都已准备就绪。

所以我们有一个实体 StepsProgression。里面有一个值对象数组“Step”。它们不存储任何东西,因此它们很简单并且非常适合作为值对象。但是为了在所有步骤中持久保存用户数据,在 StepsProgression 中还有一个对象 StepsData

麻烦来了。 StepsData 将有设置器,用于设置用户数据。所以它必须是一个实体。但从领域的角度来看,它不是一个实体。它是一个 Value 对象,因为我不关心它的身份。我可以用相同数据的对象替换它,就可以了。

在这种情况下你有什么建议?

编辑 1

再次关于域

我们确实有一个预订系统。我们询问了领域专家,我们确实有不同的步骤(或阶段)来填写一些特定的数据,以便为用户预订订单商品。所以 Step's 和 StepsProgressions 的概念是可以的。它不与 UI 耦合。例如,在 UI 端,我们同时为两个步骤填充数据。

值对象可以有 getter 和 setter。看起来在你的情况下 StepsData 描述了实体的(StepsProgression)状态,这使它成为一个值对象候选者。您可以在值对象本身中拥有一个值对象 属性。 self-contained 的值对象从根本上更易于使用。对于 DDD 纯粹主义者,值对象是不可变的、无副作用且易于测试的。

根据我对阅读你的问题/描述的稀少知识,在我看来,你正在构建某种在线商店、预订系统或类似的东西。

这个假定请仔细分析您的域。问问自己这个问题,什么 你的域到底是什么,如果 StepsProgressionStepStepData 真的是“域问题” 这样的订购系统……?

我个人感觉这些只是 UI 工作流程的抽象,根本不反映任何特定领域的概念——应用程序的纯技术视角。

在这种情况下,它们既不是实体也不是值对象,因为它们甚至不是领域模型的一部分。

我建议回到白板,首先开始对仅包含领域特定对象(+更多)的领域模型进行建模,而不要过多考虑 UI 或用例:

 - Order (Entity)
 - OrderNumber (Value Object)
 - Customer (Entity)
 - PaymentType (Entity)
 - OrderTotal (Value Object)
 - …

通过将它们组合到非常适合的聚合(事务边界),使用存储库持久化它们并使用域服务处理它们,您应该能够创建一个“丰富的”域模型。

您的应用程序的用例(即收集并保存来自用户的正确数据块中的订单相关数据)然后将由利用您现有域模型的应用程序服务进行编排。

之后可能需要对域模型进行一些较小的重构,但请记住,UI,应用程序或基础设施特定的问题应该遵循域模型而不是“泄漏”到其中。

也许我完全错误地回答了您的问题:在此情况下,对于给您带来的不便,我们深表歉意。但正如我所见,对整个领域和相关模型进行一般性的重新考虑/质疑似乎是有帮助的。

首先 re-check StepsProgression 的领域:如果我们向领域专家询问我们流程的步骤,我想我们会得到一个可以接受的答案。那么我们可以说这是我们域的一部分。

接下来,你说的这个实体还是VO?是的,它看起来像值对象,实际上是一个值对象,因为你不关心整个数据集的身份,它没有唯一的含义(如果我没有理解错的话)。

假设您有一个 UI 像 表单向导 并且每个步骤的输入都不同,我会这样实现:

public class StepsProgression
{
    private readonly string _userId;
    public Step1 step1 { get; set; }
    public Step2 step2 { get; set; }
    public Step3 step3 { get; set; }

    public StepsProgression(string userId)
    {
        _userId = userId;
    }
}
// Immutable Step Object
public class Step1
{
    public string input1 { get; }
    public string input2 { get; }
    public string input3 { get; }
}

你在第一步中实例化了 StepsProgression,所以我假设你只知道它们是谁的步。所以构造函数只设置用户 ID。然后,当用户填写 step1 输入并单击 Next 时,您将创建一个新的(不可变的)Step 对象并将其设置为 step1。如果用户单击 Back,更改 Step1 中的任何内容并再次单击 Next,那么您将再次创建一个新的 Step1 对象并将其分配给 StepsProgression。

您可以考虑更通用的实现(步骤),但由于步骤之间的输入完全不同,这种明确的方式为您提供了编码约定。

已编辑实施: 我知道 StepsProgressions 包含一组 动态 步骤,您在创建 StepsProgression 之前确定这些步骤。然后假设你事先知道 stepTypes 我会按如下方式更改我的实现:

public class StepsProgression
{
    private readonly string _userId;
    private readonly IStepProgressionBuilder _builder = new StepProgressionBuilder();
    public IEnumerable<IStep> Steps { get; set; }

    public StepsProgression(string userId, IEnumerable<string> stepTypes)
    {
        _userId = userId;
        foreach (var step in stepTypes) // foreach smells here but you got the point
        {
            _builder.AddConcreteStep(step) // step = "Step1" for instance.
        }
        Steps = _builder.Build();
    }
}
// Immutable Step Object
public class Step1 : IStep
{
    public string input1 { get; }
    public string input2 { get; }
    public string input3 { get; }
}
// Builder Pattern with Fluent Methods
public interface IStepProgressionBuilder
{
    // stepType = "step1" for instance. You have concrete Step1 class, so you return it. 
    // Start with a switch, then you refactor. (May involve some reflection)
    void AddConcreteStep(string stepType);  
    IEnumerable<IStep> Build();
}

同样,您可以使用一个通用步骤 class,但是您的构建器应该通过获取步骤应包含的每个数据来以某种方式构建一个步骤。如果您的步骤可能具有 3 个以上的属性,那么这将成为一个代码。这就是为什么我更喜欢有具体的步骤 classes,这样我就可以将它构建为一个完整的数据集。

所以最后我选择将StepsData作为值对象。它给了我 StepsProgression 之外的写保护的好处。

我可以将 StepsData 传递给 StepsProgression 之外的只读对象,不用担心,因为如果外面有人在 StepsData 上调用 setter,一个新对象将被退回。在 StepsProgression 之外,您无法重写原始 StepsData。所以即使有人调用setter,StepsProgression中原来的StepsData还是不变的。