首先让我描述一个lit-bit域。我们有一个网站,客户可以在这里下单。要下订单,客户必须提供一些数据。这个过程分为几个步骤。在每个步骤中,客户端只提供部分数据。当客户端完成最后一步时,订单所需的所有数据都准备好了。
因此,我们有一个实体StepsProgression。里面有一个值对象数组">Step"。它们不存储任何东西,因此非常简单,非常适合作为价值对象。但是,为了在所有步骤中保持用户数据,StepsProgression中还有一个对象StepsData。
麻烦来了StepsData将有setter,用于设置用户数据。所以它必须是一个实体。但从领域的角度来看,它不是一个实体。它是一个Value对象,因为我不在乎它的身份。我可以用具有相同数据的对象替换它,这是可以的
在这种情况下,你能推荐什么?
编辑1
关于域再次
我们确实有一个预订系统。我们询问了领域专家,我们确实有不同的步骤(或阶段)来填写一些特定的数据,以便为用户预订订单项目。所以Step's和StepsProgressions的概念是可以的。它没有与UI耦合。例如,在UI方面,我们同时填充两个步骤的数据。
根据我所知,通过阅读您的问题/描述,我认为您正在建立某种在线商店、预订系统或类似的系统。
假设请仔细分析您的域名。问自己一个问题,您的域到底是什么,如果StepsProgression
、Step
和StepData
真的是这样一个排序系统的">域关注点"…?
就我个人而言,我觉得这些只是UI工作流的抽象,根本没有反映任何特定领域的概念——这是应用程序的纯技术视角。
在这种情况下,它们既不是实体也不是值对象,因为它们甚至不是域模型的一部分
我建议回到白板,首先开始建模一个仅由领域特定对象(+更多)组成的领域模型,而不需要太多考虑UI或用例:
- Order (Entity)
- OrderNumber (Value Object)
- Customer (Entity)
- PaymentType (Entity)
- OrderTotal (Value Object)
- …
通过将它们组合为非常适合的聚合(事务边界),将它们持久化为存储库,并使用域服务进行处理,您应该能够创建一个"丰富"的域模型。
然后,您的应用程序的用例(即,在正确的块中收集并持久化来自用户的订单相关数据)将由使用现有域模型的应用程序服务进行编排。
之后,可能需要对域模型进行一些较小的重构,但请记住,UI、应用程序或基础设施特定的问题应该遵循域模型,而不是"泄漏"到它中
也许我完全误解了你的问题:在这种情况下,很抱歉给你带来不便。但在我看来,对整个领域和相关模式进行全面的重新考虑/质疑似乎是有帮助的。
值对象可以有getter和setter。在您的案例中,StepsData
描述了实体的(StepsProgression)状态,使其成为候选值对象。您可以在值对象本身中具有值对象属性。值对象是自包含的,这从根本上使它更容易使用。对于DDD纯粹主义者来说,值对象是不可变的,没有副作用,并且易于测试。
首先重新检查域中的StepsProgression:如果我们向域专家询问流程步骤,我认为我们得到了可以接受的答案。那么我们可以说这是我们的领域的一部分。
接下来,正如你所说,这是实体还是VO?是的,它看起来像值对象,实际上是一个值对象,因为你不在乎整个数据集的身份,它没有唯一的含义(如果我没有理解错domaing)。
假设你有一个类似的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。
您可以考虑更通用的实现(步骤),但由于步骤之间的输入都不同,因此这种明确的方式为您提供了编码约定。
编辑的实现:我知道StepsProgression包含一组动态步骤,您可以在创建StepsProgress之前确定这些步骤。然后假设您事先知道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类,这样我就可以将其构建为一个完整的数据集。
所以最后我选择将StepsData
作为Value Object。它为我提供了StepsProgression
之外的写保护的好处。
我可以在StepsProgression
之外传递StepsData
进行只读,而不用担心,因为如果有人在StepsData
上调用setter,就会返回一个新对象。在StepsProgression
之外,您不能重写原始的StepsData
。所以即使有人调用setter,StepsProgression
中原来的StepsData
仍然是不变的。