首先让我描述一个点亮的域名。我们有一个网站,客户可以在那里下订单。要下订单,客户必须提供一些数据。这个过程分为几个步骤。在每个步骤中,客户端仅提供部分数据。当客户端完成最后一步时 - 订单所需的所有数据都已准备就绪。
所以我们有一个实体 StepsProgression 。里面有一系列值对象" Step "。它们不存储任何东西,因此它们很简单,非常适合作为有价值的物品。但是为了在所有步骤中保留用户数据,在 StepsProgression 内,还有一个对象 StepsData 。
麻烦来了。 StepsData 将设置setter,以设置用户数据。所以它必须是一个实体。但从域的角度来看,它不是一个实体。它是一个价值对象,因为我不关心它的身份。我可以用具有相同数据的对象替换它,并且没关系。
在这种情况下你能推荐什么?
编辑1
再次关于域名
我们确实有预订系统。我们询问了领域专家,我们确实有不同的步骤(或阶段)来填充某些特定数据,以便为用户预订订单项目。所以Step和StepProgressions的概念是可以的。它没有与UI结合。例如,在UI方面,我们同时填充两个步骤的数据。
答案 0 :(得分:2)
根据我的罕见知识,通过阅读您的问题/描述,在我看来,您正在建立某种在线商店,预订系统或类似的东西。
这假设请仔细分析您的域名。问自己一个问题, 您的域名是什么,如果StepsProgression
,Step
和StepData
真的是“域关注”这样的订购系统......?
就我个人而言,我觉得这些只是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
仍然保持不变。