事件来源中的价值对象

时间:2018-09-16 16:26:11

标签: oop domain-driven-design event-sourcing value-objects

事件源域模型中是否有值对象的地方?

让我们将值对象定义为具有不变状态的对象,该状态保护其不变式且没有特定的标识符。

在这种情况下,事件来源域模型是完全或部分事件来源的域,这意味着其当前状态可以通过应用过去发生的所有事件来得出。事件本身甚至在一段时间内也被认为是不可变的。

关于the validity of using value objects within events的辩论已经发生-这个问题略远一点:值对象是否在事件源域中完全占有

使用值对象的(潜在)问题是,以不变式加强的方式更改域变得非常棘手。

这种情况的一个示例是有一个Username值对象,唯一的限制是名称必须介于2到16个字符之间。

虽然这已经运作了一段时间,但该公司决定只允许使用至少5个字符的用户名。 迁移期开始,要求名称少于5个字符的用户更新其名称。

可以说该过程成功了,应用了更正事件,每个人都很高兴。 我们将Username值对象的约束严格化为至少需要5个字符。

有一段时间,每个人都很高兴,但随后我们发现快照存在问题并重播所有事件。

我们现在遇到Username对象的一个​​例外:通过加载历史数据,我们正在破坏域的不变式。

值对象的规则追溯应用-这是否使它们天生就不适合用于事件源?值对象的版本控制是否值得?有避免这种问题的更简单方法吗?

4 个答案:

答案 0 :(得分:3)

我想说的是,当您重新定义Username的含义时,并且您不会以某种方式迁移历史数据,实际上已经创建了两个不同的Username意义。

由于该单词有两种不同的含义,因此您必须以某种方式使其在代码中明确。 “版本控制”是一种方法,尽管我不会使用这种通用解决方案,但是有不同的建模选项。

您可以明确地说“用户名”的历史就是历史。因此,例如,创建一个HistoricUsername,它是事件源对象,如果需要的话甚至可以是值对象。并创建一个Username,它始终是具有最新规则的用户名,它根本不会保留,而是从HistoricUsername创建(如果可以)。

有些人建议有时从对象中提取“规则”,然后在以后重新应用。这样,对象本身始终有效,您可以要求它针对可能更改的规则进行验证。我不太喜欢这类解决方案,但这是一个选择,Username仍将是一个价值对象。

因此,问题并不在于价值对象不适合事件来源,而在于建模必须更加准确。

答案 1 :(得分:2)

  

值对象在事件源域中是否有位置?

是的

  

有避免这种问题的更简单方法吗?

“不要那样做。”

您描述的问题实际上是与消息传递有关的问题-如果我们对消息进行向后不兼容的更改,那么事情就会中断。

(更确切地说,您有一条“用户名”消息,并且您正在尝试使用一组新的约束来重复使用该消息,这些约束拒绝了该消息的某些先前有效的用法。)

答案是,您不会引入向后不兼容的更改-而是引入与新要求相匹配的新名称,而不推荐使用旧名称。

也就是说,增加对新消息的支持并删除对旧消息的支持,成为两个单独管理的选项。

格雷格·杨(Greg Young)的书Versioning in an Event Sourced System专门介绍了这一思想。另外,Rich Hickey在他的大多数演讲中都谈到了这些重要的想法-我建议从Spec-ulation开始。

“值对象”,意味着域模型的当前实现用来移动信息的类型是与消息分开的关注点。我们在内存中使用的数据结构不需要与我们的序列化格式耦合。

在线上信息的表示形式不同于内存中的信息表示形式,而又不同于操纵内存中的信息的抽象形式。

具有挑战性的事情是,在项目开始时,关于不同表示何时将出现分歧的信息最少。

答案 2 :(得分:2)

我们以略有不同的方式解决了这个问题。通过将价值对象的 public API与内部(仅限域)API分开,我们可以在不影响其他API的情况下发展它们。

例如:

public class Username
{
    private readonly string value;

    // Domain-only (internal) constructor.
    // Does not enforce constriants and can only be called within the domain.
    internal Username(string value)
    {
        this.value = value;
    }

    // Public factory method.
    // Enforces business constraints. Used by consumers of the domain (application layer etc.)
    // to create new instances of the value object.
    public static Username Create(string value)
    {
        // Business constraints. These will evolve and grow over time.
        if (value == null)
        {
            // throw exception etc.
        }

        if (value.Length < 2)
        {
            // throw exception etc.
        }

        return new Username(value);
    }
}

域的消费者必须使用静态Create方法来创建值对象的新实例。此工厂方法包含我们所有的业务约束,并防止在无效状态下创建实例。

在域中,类可以访问内部(无约束)构造函数。由于这不会强制执行任何业务约束,因此始终可以以这种方式创建价值对象的实例(无论其价值如何)。通过在重播事件时使用此构造函数,我们可以确保历史数据将始终成功。

这种设计的好处是:

  • 使用单个类来表示域概念(不需要多个类,版本控制等)。
  • 业务规则可以随时间自由发展。
  • 历史数据始终有效。即使我们的规则已更改,一年前的Username仍然是用户名。

答案 3 :(得分:1)

尽管已经回答了,但我确实发现了一种有趣的情况。

我与其他人一样,事件数据应基于记录,因此,仅是可用于重构聚合的数据容器。

也就是说,规则更改时域也会更改。域驱动设计的主要部分是捕获所需的尽可能多的域(规则/结构)。如果是这种情况,规则的更改是否也应保留?

例如,如果我们有一个Username 值对象,并且它以2到16个字符的规则开头,则编码如下:

public class Username
{
    public string Value { get; }

    public Username(string value)
    {
        if (value.Length < 2 || value.Length > 16)
        {
            throw new DomainException("Username must be between 2 and 16 characters");
        }

        Value = value;
    }
}

现在我们到 2018年3月1日,规则将更改。我们可以遵循以下规则:

public class Username
{
    public string Value { get; }

    public Username(string value, DateTime registrationDate)
    {
        if (registrationDate < new Date(2018, 3, 1) &&
            (value.Length < 2 || value.Length > 16))
        {
            throw new DomainException("Username must be between 2 and 16 characters");
        }

        if (registrationDate >= new Date(2018, 3, 1) &&
            (value.Length < 5 || value.Length > 16))
        {
            throw new DomainException("Username must be between 5 and 16 characters");
        }

        Value = value;
    }
}

那是基本思想。这样,我们也遵守了我们的“旧”规则。这个可能很麻烦,但是我没有足够的经验可以说。追溯地更改我们的规则可能会带来一些棘手的情况,所以我想一个人需要逐案评估。

只是一个想法。