事件溯源中的价值对象
Value object in event sourcing
在事件源域模型中是否有值对象的位置?
让我们将值对象定义为具有不可变状态的对象,它保护其不变量并且没有特定的标识符。
在此上下文中,事件源域模型是一个完全或部分事件源的域,这意味着它的当前状态可以通过应用发生在过去。事件本身被认为是不可变的,即使随着时间的推移也是如此。
关于 the validity of using value objects within events 的争论已经发生 - 这个问题稍微进一步:值对象在事件源域中是否占有一席之地 ?
使用值对象的(潜在)问题是,以不变量收紧的方式更改域变得相当棘手。
这种情况的一个例子是有一个 Username
值对象,唯一的限制是名称必须介于 2 到 16 个字符之间。
虽然这已经运行了一段时间,但公司决定只允许至少 5 个字符的用户名。
迁移期开始,姓名少于 5 个字符的用户将被要求更新他们的姓名。
可以说这个过程是成功的,应用了更正事件,每个人都很高兴。
我们收紧了对 Username
值对象的限制,要求至少包含 5 个字符。
有一段时间每个人都很高兴,但后来我们发现快照有问题并重播所有事件。
我们现在面临 Username
对象的异常:通过加载历史数据,我们打破了域的不变量。
值对象的规则追溯适用 - 这是否使它们本质上不适合事件溯源?是否值得应用值对象的版本控制?有没有更简单的方法来避免此类问题?
我想说的是,目前您重新定义了 Username
的含义, 和 您并没有以某种方式迁移历史数据,您实际上已经创建了 2 个不同的Username
个含义。
因为这个词有两种不同的含义,所以你必须以某种方式在代码中明确说明。 "Versioning" 是一种方法,虽然我不会使用这种通用解决方案,但有不同的建模选项。
您可以明确表示 "username" 的历史就是这样,一段历史。因此,例如创建一个 HistoricUsername
,它是事件源对象,如果需要,甚至可以是值对象。并创建一个 Username
,它始终是具有最新规则的用户名,它根本不会持久化,但如果可以的话,它是从 HistoricUsername
创建的。
有些人建议有时从对象中提取 "rules",然后再重新应用。这样对象本身在任何时候都是有效的,你可以要求它根据可能改变的规则来验证自己。我不太喜欢这些解决方案,但它是一个选项,Username
仍然是一个值对象。
所以问题不在于值对象不适合事件溯源,而在于建模必须更加准确。
Do value objects have a place in event sourced domains at all?
是的。
Is there a simpler way of avoiding such problems?
"Don't do that."
您所描述的问题实际上是一个关于消息传递的问题 - 如果我们对我们的消息进行向后不兼容的更改,那么事情就会崩溃。
(更准确地说,您有一条 "Username" 消息,并且您正尝试使用一组新的约束来重新使用该消息,这些约束会拒绝该消息以前的一些有效使用)。
答案是您不会引入向后不兼容的更改 - 而是引入符合新要求的新名称,并弃用旧名称。
也就是说,添加对新消息的支持和删除对旧消息的支持,成为两个单独管理的选项。
Greg Young 的书 Versioning in an Event Sourced System dedicates some chapters to this idea. Also, Rich Hickey ends up touching on these important ideas in most of his talks -- I'd suggest starting from Spec-ulation。
"value object",意思是域模型的当前实现用来移动信息的类型,是一个独立于消息的关注点。我们在内存中使用的数据结构不需要耦合到我们的序列化格式。
线上信息的表示不同于内存中信息的表示,而后者又不同于操作内存中信息的抽象。
具有挑战性的是,在项目开始时,关于不同表示何时会出现分歧的信息最少。
虽然已经回答了,但我确实发现这是一个有趣的情况。
我同意其他人的观点,事件数据应该基于记录,因此,只不过是一个可用于重构聚合的数据容器。
也就是说,当规则发生变化时,域也会发生变化。领域驱动设计的主要部分是根据需要捕获尽可能多的领域 (rules/structure)。如果是这样的话,规则的变化是不是也应该保留?
例如,如果我们有一个 Username
Value Object 并且它以 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;
}
}
这是基本思路。通过这种方式,我们也保留了 "old" 规则。这可能会变得很麻烦,但我没有足够的经验可以说。追溯更改我们的规则可能会引入一些非常棘手的情况,因此我想需要根据具体情况进行评估。
只是一个想法。
我们用稍微不同的方式解决了这个问题。通过将值对象的 public 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
方法来创建值对象的新实例。此工厂方法包含我们所有的业务约束并防止在无效状态下创建实例。
在域内,classes 可以访问内部(无约束)构造函数。由于这不会强制执行任何业务约束,因此始终可以以这种方式创建值对象的实例(无论其值如何)。通过在重放事件时使用此构造函数,我们可以确保历史数据始终成功。
这种设计的好处是:
- 单个class用于表示域概念(不需要多个classes、版本控制等)。
- 业务规则可以随时间自由发展。
- 历史数据总是有用的。一年前的
Username
仍然是用户名 ,即使我们的规则已更改。
在事件源域模型中是否有值对象的位置?
让我们将值对象定义为具有不可变状态的对象,它保护其不变量并且没有特定的标识符。
在此上下文中,事件源域模型是一个完全或部分事件源的域,这意味着它的当前状态可以通过应用发生在过去。事件本身被认为是不可变的,即使随着时间的推移也是如此。
关于 the validity of using value objects within events 的争论已经发生 - 这个问题稍微进一步:值对象在事件源域中是否占有一席之地 ?
使用值对象的(潜在)问题是,以不变量收紧的方式更改域变得相当棘手。
这种情况的一个例子是有一个 Username
值对象,唯一的限制是名称必须介于 2 到 16 个字符之间。
虽然这已经运行了一段时间,但公司决定只允许至少 5 个字符的用户名。 迁移期开始,姓名少于 5 个字符的用户将被要求更新他们的姓名。
可以说这个过程是成功的,应用了更正事件,每个人都很高兴。
我们收紧了对 Username
值对象的限制,要求至少包含 5 个字符。
有一段时间每个人都很高兴,但后来我们发现快照有问题并重播所有事件。
我们现在面临 Username
对象的异常:通过加载历史数据,我们打破了域的不变量。
值对象的规则追溯适用 - 这是否使它们本质上不适合事件溯源?是否值得应用值对象的版本控制?有没有更简单的方法来避免此类问题?
我想说的是,目前您重新定义了 Username
的含义, 和 您并没有以某种方式迁移历史数据,您实际上已经创建了 2 个不同的Username
个含义。
因为这个词有两种不同的含义,所以你必须以某种方式在代码中明确说明。 "Versioning" 是一种方法,虽然我不会使用这种通用解决方案,但有不同的建模选项。
您可以明确表示 "username" 的历史就是这样,一段历史。因此,例如创建一个 HistoricUsername
,它是事件源对象,如果需要,甚至可以是值对象。并创建一个 Username
,它始终是具有最新规则的用户名,它根本不会持久化,但如果可以的话,它是从 HistoricUsername
创建的。
有些人建议有时从对象中提取 "rules",然后再重新应用。这样对象本身在任何时候都是有效的,你可以要求它根据可能改变的规则来验证自己。我不太喜欢这些解决方案,但它是一个选项,Username
仍然是一个值对象。
所以问题不在于值对象不适合事件溯源,而在于建模必须更加准确。
Do value objects have a place in event sourced domains at all?
是的。
Is there a simpler way of avoiding such problems?
"Don't do that."
您所描述的问题实际上是一个关于消息传递的问题 - 如果我们对我们的消息进行向后不兼容的更改,那么事情就会崩溃。
(更准确地说,您有一条 "Username" 消息,并且您正尝试使用一组新的约束来重新使用该消息,这些约束会拒绝该消息以前的一些有效使用)。
答案是您不会引入向后不兼容的更改 - 而是引入符合新要求的新名称,并弃用旧名称。
也就是说,添加对新消息的支持和删除对旧消息的支持,成为两个单独管理的选项。
Greg Young 的书 Versioning in an Event Sourced System dedicates some chapters to this idea. Also, Rich Hickey ends up touching on these important ideas in most of his talks -- I'd suggest starting from Spec-ulation。
"value object",意思是域模型的当前实现用来移动信息的类型,是一个独立于消息的关注点。我们在内存中使用的数据结构不需要耦合到我们的序列化格式。
线上信息的表示不同于内存中信息的表示,而后者又不同于操作内存中信息的抽象。
具有挑战性的是,在项目开始时,关于不同表示何时会出现分歧的信息最少。
虽然已经回答了,但我确实发现这是一个有趣的情况。
我同意其他人的观点,事件数据应该基于记录,因此,只不过是一个可用于重构聚合的数据容器。
也就是说,当规则发生变化时,域也会发生变化。领域驱动设计的主要部分是根据需要捕获尽可能多的领域 (rules/structure)。如果是这样的话,规则的变化是不是也应该保留?
例如,如果我们有一个 Username
Value Object 并且它以 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;
}
}
这是基本思路。通过这种方式,我们也保留了 "old" 规则。这可能会变得很麻烦,但我没有足够的经验可以说。追溯更改我们的规则可能会引入一些非常棘手的情况,因此我想需要根据具体情况进行评估。
只是一个想法。
我们用稍微不同的方式解决了这个问题。通过将值对象的 public 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
方法来创建值对象的新实例。此工厂方法包含我们所有的业务约束并防止在无效状态下创建实例。
在域内,classes 可以访问内部(无约束)构造函数。由于这不会强制执行任何业务约束,因此始终可以以这种方式创建值对象的实例(无论其值如何)。通过在重放事件时使用此构造函数,我们可以确保历史数据始终成功。
这种设计的好处是:
- 单个class用于表示域概念(不需要多个classes、版本控制等)。
- 业务规则可以随时间自由发展。
- 历史数据总是有用的。一年前的
Username
仍然是用户名 ,即使我们的规则已更改。