【问题标题】:DDD: Entity technically, but looks like value object?DDD:从技术上讲是实体,但看起来像值对象?
【发布时间】:2017-02-10 11:34:49
【问题描述】:

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

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

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

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

编辑 1

再次关于域

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

【问题讨论】:

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


    【解决方案1】:

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

    假设请仔细分析您的域。问问自己这个问题,什么您的域到底是什么,如果StepsProgressionStepStepData 真的是这种排序系统的“域问题”......?

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

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

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

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

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

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

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

    也许我完全弄错了您的问题:在这种情况下,很抱歉给您带来不便。但在我看来,这是对整个领域的普遍重新考虑/质疑,相关模型似乎很有帮助。

    【讨论】:

    • 感谢您的回复!我们确实在谈论预订系统。而为预订填写数据的过程,分为几个步骤,是我们的核心领域。这是我们超越竞争对手的优势。并且步骤模型与 UI 非常分离(因为在 UI 中许多步骤是统一的并位于一个页面上)。所以你的建议和一般建议一样好,但我们已经有了单独的订单模型和单独的步骤模型。
    【解决方案2】:

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

    【讨论】:

    • 小心设置器:VO 是不可变的,所以设置器必须返回一个新对象
    • 谢谢! StepsData 确实有点像状态,因为当我们向它填充一些东西时,我们会改变状态。所以看起来我可以让 StepsData 成为一个带有 setter 的值对象,这将返回一个新的值对象,我将用它来替换旧的?
    【解决方案3】:

    首先重新检查域的 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。如果用户单击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();
    }
    

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

    【讨论】:

    • 感谢您的回复。我们询问了领域专家,Steps 是可以的,但是 Steps 没有与 UI 耦合。我喜欢你的例子,但我希望在创建 StepsProgression 时已经有 Steps 的实例。因为 Steps Progression 可以有许多不同的 step,所以一个 StepsProgression 可以与另一个完全不同。我需要一种在一个 StepsProgression 中访问所有步骤的所有数据的方法,这就是需要 StepsData 的原因。
    • 我了解您的 StepsProgression 包含一组“动态”步骤,这些步骤可能会有所不同,因为表单向导不同,并且您想使用通用 StepsProgression 类。如果我没记错的话,我编辑了根据您的需求解决方案。
    【解决方案4】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-10-04
      • 2016-03-23
      • 2012-07-08
      • 2012-04-01
      • 1970-01-01
      • 2017-02-09
      • 2010-11-25
      相关资源
      最近更新 更多