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

时间:2017-02-10 11:34:49

标签: design-patterns architecture entity domain-driven-design value-objects

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

所以我们有一个实体 StepsProgression 。里面有一系列值对象" Step "。它们不存储任何东西,因此它们很简单,非常适合作为有价值的物品。但是为了在所有步骤中保留用户数据,在 StepsProgression 内,还有一个对象 StepsData

麻烦来了。 StepsData 将设置setter,以设置用户数据。所以它必须是一个实体。但从域的角度来看,它不是一个实体。它是一个价值对象,因为我不关心它的身份。我可以用具有相同数据的对象替换它,并且没关系。

在这种情况下你能推荐什么?

编辑1

再次关于域名

我们确实有预订系统。我们询问了领域专家,我们确实有不同的步骤(或阶段)来填充某些特定数据,以便为用户预订订单项目。所以Step和StepProgressions的概念是可以的。它没有与UI结合。例如,在UI方面,我们同时填充两个步骤的数据。

4 个答案:

答案 0 :(得分:2)

根据我的罕见知识,通过阅读您的问题/描述,在我看来,您正在建立某种在线商店,预订系统或类似的东西。

这假设请仔细分析您的域名。问自己一个问题, 您的域名是什么,如果StepsProgressionStepStepData真的是“域关注”这样的订购系统......?

就我个人而言,我觉得这些只是UI工作流程的抽象,并没有反映任何领域特定的概念 - 从应用程序的纯技术角度来看。

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

我建议回到白板并首先开始建模一个只包含域特定对象的域模型(+更多),而不会过多地考虑UI或用例:

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

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

您的应用程序的用例(即以正确的块收集和保存用户的相关数据)将由使用您现有域模型的Applications Services进行编排。

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

也许我的问题完全错了:在这种情况下很抱歉给您带来不便。但是,正如我所看到的那样,对整个域和对模型的一般性重新考虑/质疑似乎是有帮助的。

答案 1 :(得分:1)

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

答案 2 :(得分:0)

首先重新检查域名中的StepsProgression:如果我们向域专家询问我们的流程步骤,我认为我们得到了一个可接受的答案。然后我们可以说这是我们领域的一部分。

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

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

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 。如果用户单击返回,则从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();
}

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

答案 3 :(得分:0)

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

我可以在StepsData之外传递StepsProgression,只读而不用担心,因为如果外面有人会在StepsData上调用一个setter,则会返回一个新对象。在StepsProgression之外,您无法重写原始StepsData。因此,即使有人调用了setter,StepsData中的原始StepsProgression仍然保持不变。