在事件源的域模型中是否有值对象的位置
让我们将值对象定义为具有不可变状态的对象,该对象保护其不变量,并且没有特定的标识符。
在此上下文中,事件源域模型是完全或部分源于事件的域,这意味着它的当前状态可以通过应用过去发生的所有事件来派生。事件本身被认为是不可变的,即使随着时间的推移也是如此。
关于在事件中使用值对象的有效性,已经发生了争论——这个问题更进一步:值对象在事件源域中是否有一席之地?
使用值对象的(潜在)问题是,以收紧不变量的方式更改域变得相当棘手。
这个场景的一个例子是有一个Username
值对象,唯一的约束是名称必须在2到16个字符之间。
虽然这已经运行了一段时间,但业务部门决定只允许使用至少5个字符的用户名。迁移期开始,名称少于5个字符的用户将被要求更新其名称。
比方说,这个过程是成功的,应用了纠正事件,每个人都很高兴。我们收紧了对Username
值对象的约束,使其至少需要5个字符。
有一段时间,每个人都很高兴,但后来我们发现快照有问题,并回放所有事件。
我们现在面临来自Username
对象的一个异常:通过加载历史数据,我们打破了域的一个不变量。
值对象的规则可追溯应用-这是否使它们天生不适合事件源?是否值得应用价值对象的版本控制?有没有一种更简单的方法可以避免这些问题?
我想说,在您重新定义Username
的含义时,和您没有以某种方式迁移历史数据,实际上您已经创建了两种不同的Username
含义。
因为这个词有两种不同的含义,所以你必须以某种方式在代码中明确它。"版本控制"是一种方法,尽管我不会使用这样的通用解决方案,但有不同的建模选项。
你可以明确地说,"用户名"的历史就是这样,一个历史。例如,创建一个HistoricUsername
,它是源于事件的对象,如果需要的话,甚至是一个值对象。并创建一个Username
,它始终是具有最新规则的用户名,它根本不持久化,但如果可以的话,它是从HistoricUsername
创建的。
有些人有时建议从对象中提取"规则",然后重新应用。这样,对象本身在任何时候都是有效的,您可以要求它根据可能更改的规则进行验证。我并不喜欢这类解决方案,但这是一种选择,Username
仍然是一个有价值的对象。
因此,问题并不是价值对象不适合事件源,只是建模必须更准确。
我们用稍微不同的方式解决了这个问题。通过将我们的值对象的公共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
仍然是用户名,即使我们的规则已经更改
值对象在事件源域中有位置吗?
是。
有没有更简单的方法可以避免此类问题?
"不要那样做。">
你所描述的问题实际上是关于消息传递的——如果我们对消息进行向后不兼容的更改,那么事情就会崩溃。
(更准确地说,您有一条"用户名"消息,您正试图使用一组新的约束来重复使用该消息,这些约束拒绝了对该消息的一些以前有效的使用)。
答案是,您不会引入向后不兼容的更改,而是引入符合新要求的新名称,并弃用旧名称。
也就是说,添加对新消息的支持和删除对旧消息的支持成为两个单独管理的选项。
Greg Young的《事件源系统中的版本控制》一书专门用了一些章节来阐述这个想法。此外,Rich Hickey在他的大部分演讲中都提到了这些重要的想法——我建议从Speculation开始。
"值对象"是指域模型的当前实现用于移动信息的类型,它与消息是一个单独的关注点。我们在内存中使用的数据结构不需要与序列化格式耦合。
导线上信息的表示不同于内存中信息的表示,这反过来又不同于操作内存中信息信息的抽象。
具有挑战性的是,在项目开始时,您对不同表示何时会出现分歧的信息量最少。
虽然已经回答了,但我确实觉得这是一个有趣的情况。
我同意其他人的观点,即事件数据应该是基于记录的,因此,它只不过是一个可用于重建聚合的数据容器。
话虽如此,但当规则发生变化时,域名也会发生变化。领域驱动设计的主要部分是根据需要捕获尽可能多的领域(规则/结构)。如果是这种情况,规则中的更改是否也应该保留?
例如,如果我们有一个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;
}
}
这就是基本理念。通过这种方式,我们也保留了我们的"旧"规则。这个可能会变得很麻烦,但我没有足够的经验。追溯性地改变我们的规则可能会带来一些相当棘手的情况,所以我想我们需要根据具体情况进行评估。
只是一个想法。