代码之家  ›  专栏  ›  技术社区  ›  Stratadox

事件源中的值对象

  •  2
  • Stratadox  · 技术社区  · 7 年前

    事件源域模型中是否有值对象的位置?

    让我们定义一个 价值对象性

    事件来源 在此上下文中,域模型是一个完全或部分来自事件的域,这意味着它的当前状态可以从应用过去发生的所有事件中派生出来。事件本身被认为是不可变的,即使是随着时间的推移。

    the validity of using value objects within events -这个问题更进一步:值对象在事件源域中有位置吗 完全 ?

    使用值对象的(潜在的)问题是,以固定不变量的方式更改域变得相当棘手。

    这种情况的一个例子是 Username

    虽然这已经运行了一段时间,但业务部门决定只允许至少5个字符的用户名。 迁移期开始,名称少于5个字符的用户被要求更新其名称。

    我们加强了对我们国家的限制 用户名

    我们现在面临着一个例外 用户名 对象:通过加载历史数据,我们打破了域的不变量。

    价值对象的规则 追溯适用 -这是否使他们天生就不适合活动采购?是否值得应用值对象的版本控制?有没有更简单的方法来避免这些问题?

    4 回复  |  直到 7 年前
        1
  •  4
  •   Robert Bräutigam    7 年前

    我想说,在你重新定义 Username 意味着, 您不需要迁移历史数据,实际上您已经创建了两个不同的 用户名 意义。

    因为这个词有两种不同的含义,所以你必须在代码中明确它。”“版本控制”是一种方法,虽然我不会使用这样的通用解决方案,但有不同的建模选项。

    你可以明确的说“用户名”的历史就是一个历史。例如,创建一个 HistoricUsername ,它是事件源对象,如果需要,甚至是值对象。创建一个 用户名 它始终是具有最新规则的用户名,它根本不会持久化,而是从 如果可以的话。

    有些人有时建议从对象中提取“规则”,然后再重新应用。这样,对象本身在任何时候都是有效的,您可以要求它根据可能更改的规则验证自身。我真的不喜欢这种解决方案,但这是一种选择,而且 仍将是一个值对象。

    所以问题并不在于值对象不适合事件源,而是建模必须更精确。

        2
  •  3
  •   VoiceOfUnreason    7 年前

    对。

    “别那么做。”

    您所描述的问题实际上是一个关于消息传递的问题—如果我们对消息进行向后不兼容的更改,那么事情就会破裂。

    (更准确地说,您有一个“Username”消息,并且您正尝试使用一组新的约束重新使用该消息,这些约束拒绝了该消息以前的一些有效使用)。

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

    格雷格·杨的书 Versioning in an Event Sourced System Spec-ulation .

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

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

        3
  •  3
  •   Alex Justus    7 年前

    我们用稍微不同的方法解决了这个问题。通过分离 我们的价值对象的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 方法创建值对象的新实例。此工厂方法包含所有业务约束,并防止在无效状态下创建实例。

    在域内部,类可以访问内部(无约束)构造函数。因为这不强制任何业务约束,所以可以始终以这种方式创建value对象的实例(不管其值如何)。通过在重放事件时使用此构造函数,我们可以确保历史数据总是成功的。

    这种设计的好处是:

    • 单个类用于表示域概念(不需要多个类、版本控制等)。
    • 业务规则可以随时间自由发展。
    • Username 从一年前开始 仍然是用户名
        4
  •  1
  •   Eben Roux    7 年前

    虽然我已经回答了,但我确实觉得这是一个有趣的情况。

    记录 -因此,只不过是一个数据容器,可用于重新构建聚合。

    也就是说,当规则改变时,域也会改变。领域驱动设计的一个主要部分是根据需要捕获尽可能多的领域(规则/结构)。如果是这样的话,规则的变化是否也应该保留?

    例如,如果我们有 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;
        }
    }
    

    这是基本的想法。这样,我们也保持了我们的“旧”规则。这个 可以

    只是一个想法。