我读过类似的问题DDD中是否有关于共享值对象的概念和ddd中的值对象设计规则,当
VO
是100%共享时它们都有意义。
我有多个域实体共享相同的
Name
值对象 (VO
) 概念。
public record Name
{
public string Value { get; }
private const uint MinLength = ValidationConstants.MinNameLength;
private const uint MaxLength = ValidationConstants.MaxNameLength;
public Name(string value)
{
value = value.Trim();
if (!value.LengthIsBetween(MinLength, MaxLength))
{
throw new InvalidResourceNameException(
$"Name must be provided and be between {MinLength} and {MaxLength} characters (inclusive)"
);
}
Value = value;
}
}
MinNameLength
和 MaxNameLength
允许长度中的实体之间的概念有何不同。在构造 Name
时,我 could 提供 MinNameLength
和 MaxNameLength
长度值,但这感觉不对,因为 VO
应该负责其自身的不变量(如果我错了,请纠正我?)。
为每个实体重复
DDD
Name
是否更符合 VO
,或者定义一个可以扩展以满足实体特定要求的 Name
VO
?
public record EntityName : Name
{
// Entity specific stuff
}
MinNameLength 和 MaxNameLength 允许长度中的实体之间的概念有何不同。在构造名称时,我可以提供 MinNameLength 和 MaxNameLength 长度值,但这感觉不对,因为 VO 应该对其自己的不变量负责(如果我错了,请纠正我?)。
TL;DR - 如果您有两个不同的策略,您可能有两个不同的“值对象”,并且在您希望静态分析工具可以检测何时传递“错误”值对象的编程环境中,那么这将通常意味着两种不同的类型。
如果这两种不同的类型足够相似,那么它们可能有一个共同的基本类型,或者它们可能有共同的特征/混合——这将有助于减少工作中的“重复”量。
好的,更长的版本:
public record Name
{
public string Value { get; }
// ...
}
这里有一个大谜团:客户可以对 Name.Value 做出哪些假设?
我们这里真正讨论的是Name的合约;如果客户端满足合约的所有先决条件,Name 类型承诺交换的后置条件是什么?
如果我们有一个上下文承诺 Name.Value 将少于 10 个字符,而另一个上下文承诺 Name.Value 将超过 10 个字符,那么我们就有两个不同的合约。
如果我们有两个不同的合约,那么我们通常会需要两种不同的类型,以便机器的自动检查可以检测到将一个合约的消费者与不同合约的提供者连接起来的故障。
(当然,这些不是 DDD 想法——其血统可以追溯到 1969 年的 Hoare;在“面向对象”的背景下,您更有可能听到用 Bertrand Meyer 的“契约设计”语言表达的这些想法。 )
所以我们在这里看到的是:
public Name(string value)
{
value = value.Trim();
if (!value.LengthIsBetween(MinLength, MaxLength))
{
throw new InvalidResourceNameException(
$"Name must be provided and be between {MinLength} and {MaxLength} characters (inclusive)"
);
}
Value = value;
}
尝试“动态地”表达 Name 构造函数的“前提条件”。也就是说,为了保证Name能够履行其整个契约(包括上面描述的Name.Value的后置条件),那么Name构造函数的调用者必须传递一个满足某些前置条件的字符串Value。 Name 构造函数在这里所做的部分作用是充当防火墙 - 我们无法(由于我们用于实现解决方案的编程语言的设计)表达对通用字符串数据类型的静态约束,因此相反,我们创建我们的值类型,并对此有一堆静态验证的约束,唯一的 string -> Name 函数具有动态检查。 现在是坏消息:上面的代码遵循为检测程序员错误而设计的模式。
这些模式看起来也适合清理不受信任的输入;但这并不是完全相同的问题,并且根本不清楚我们是否应该使用一个问题的解决方案来解决另一个问题。
(推荐阅读:
解析,不验证作者:Alexis King。)
表达紧张的一种方式:程序员错误不应该发生,因此肯定符合“例外”条件,并且我们当然希望我们的设计不被一堆显式错误处理代码弄乱。另一方面,无效输入当然不是异常情况,尤其是在系统边界附近,因此不太清楚异常是否是设计我们的卫生代码的正确方法。
(当然,无论哪种方式都“有效”,因为计算机将正确解释指令。我们真正讨论的是设计的含义,以及不同的权衡对短期和长期开发人员生产力的影响.)