Объект значения в источнике событий

Есть ли место для объектов значений в модели предметной области с источником событий?

Давайте определим объект значения как объект с неизменяемым состоянием, которое защищает его инварианты и не имеет определенного идентификатора.

Модель домена источника событий в этом контексте - это домен, который полностью или частично является источником событий, что означает, что его текущее состояние может быть получено путем применения всех событий, которые произошли в прошлом. Сами события считаются неизменными даже во времени.

Состоялись дебаты по поводу допустимости использования объектов значений в событиях - этот вопрос идет немного дальше: есть ли вообще место для объектов значений в доменах с источником событий вообще?

(Потенциальная) проблема с использованием объектов-значений состоит в том, что становится довольно сложно изменить предметную область таким образом, чтобы инварианты были ужесточены.

Примером этого сценария может быть объект со значением Username с единственным ограничением: имя должно содержать от 2 до 16 символов.

Хотя это уже некоторое время работает хорошо, компания решает разрешить имена пользователей не менее 5 символов. Начинается период миграции, и пользователей с именами менее 5 символов просят обновить свои имена.

Допустим, процесс прошел успешно, применяются корректирующие события, и все довольны. Мы ужесточили ограничения для нашего объекта Username value, чтобы он требовал не менее 5 символов.

Какое-то время все довольны, но затем мы обнаруживаем проблему со снимками и воспроизводим все события.

Теперь мы сталкиваемся с исключением из нашего Username объекта: загружая исторические данные, мы нарушаем инвариант нашего домена.

Правила объектов значения применяются задним числом - делает ли это их по своей сути непригодными для поиска событий? Стоит ли применять управление версиями объектов значений? Есть ли более простой способ избежать таких проблем?


person Stratadox    schedule 16.09.2018    source источник
comment
Правила объектов-значений применяются задним числом - я не понимаю этой части. Как вы думаете, что в этом отношении особенного для ВО, что отличает их от сущностей / объектов предметной области?   -  person guillaume31    schedule 17.09.2018
comment
Часть об инвариантах напоминает мне этот пост в блоге .   -  person guillaume31    schedule 17.09.2018
comment
@ guillaume31 Ретроактивность в этом контексте означает, что правила также будут применяться к данным о событиях в прошлом. Предыдущие события были действительны для старой версии объекта значения, но обновление в объекте значения делает прошлые события недопустимыми в глазах нового кода. Другие объекты не имеют этой проблемы, если они не проверяют свое состояние при построении.   -  person Stratadox    schedule 17.09.2018
comment
Кроме того, если я правильно понимаю вашу предпосылку, даже если не на конструкции, все инварианты все равно должны быть проверены, когда объект переходит в другое состояние ... так же и при воспроизведении событий, верно? Я действительно не уверен в том, какое различие вы проводите между инвариантами сущностей и правилами VO в этом отношении.   -  person guillaume31    schedule 17.09.2018
comment
Я полагаю, что вопрос касается всех объектов, которые проверяют свое состояние при применении всех прошлых событий. Я назвал объекты значений по имени, потому что они являются наиболее распространенным вариантом использования и потому, что они являются предметом обсуждения, из которого возник этот вопрос.   -  person Stratadox    schedule 17.09.2018
comment
Мое предложение было бы не проверять правила домена при применении прошлого события. По крайней мере, не для полудоменных полуприменимых правил, таких как максимальная длина строки, которые в любом случае не оказывают большого влияния на целостность агрегата.   -  person guillaume31    schedule 17.09.2018
comment
Длина имени - это упрощенный пример; это также может применяться к гораздо более специфичным для домена правилам, таким как повышенный минимальный депозит или устаревший игровой персонаж.   -  person Stratadox    schedule 17.09.2018
comment
Предложение не проверять правила домена при применении прошлого события, кажется, указывает на то, что объектам значений нет места в источнике событий, поскольку особенно те, которые всегда проверяют свои инварианты при (пере) построении. Если такова ваша точка зрения, я искренне приветствую ответ с этой стороны.   -  person Stratadox    schedule 18.09.2018
comment
1 / Опять же, я не вижу фундаментальной разницы между объектами значений и, скажем, дочерними объектами. 2 / в то время как литература DDD настаивает на том, что должны делать VO, когда запрашивается их публичная часть, и вы правы, говоря, что они должны проверять свою целостность при создании, в ней редко говорится о том, как восстановить их из постоянного хранилища, оставив это на усмотрение разработчик и конкретную технологию персистентности, которую они используют. Но это именно то, о чем ваш Q - воспроизведение событий для создания агрегата из сохраненных событий и может следовать совершенно другому пути кода.   -  person guillaume31    schedule 18.09.2018
comment
3 / Да, некоторые из корифеев DDD не проверяют правила домена при применении события - независимо от того, является ли оно новостью для ВО или нет. См. здесь и там и это Грег Янг, видео.   -  person guillaume31    schedule 18.09.2018
comment
4 / Я больше знаком с функциональными реализациями CQRS / ES, где границы между сущностью и VO более размыты. Но даст шанс ответить.   -  person guillaume31    schedule 18.09.2018


Ответы (4)


Я бы сказал, что в тот момент, когда вы переопределили значение Username, и вы каким-то образом не переносите исторические данные, вы, по сути, создали два разных Username значения.

Поскольку у этого слова есть два разных значения, вам нужно как-то явно указать в коде. «Управление версиями» - это один из способов, хотя я бы не стал использовать такое универсальное решение, существуют разные варианты моделирования.

Вы могли бы явно указать, что история «имени пользователя» - это всего лишь история. Так, например, создайте HistoricUsername, который является объектом, источником которого является событие, даже если хотите, объект-значение. И создайте Username, которое всегда является именем пользователя с самыми последними правилами, которое вообще не сохраняется, а создается из HistoricUsername, если это возможно.

Некоторые люди предлагают иногда извлекать «правила» из объекта и применять их позже. Таким образом, сам объект действителен в любое время, и вы можете попросить его проверить себя на соответствие правилам, которые могут измениться. Я не особо предпочитаю такие решения, но это вариант, и Username все равно будет объектом-значением.

Так что проблема не в том, что объекты-значения не подходят для источников событий, а в том, что моделирование должно быть более точным.

person Robert Bräutigam    schedule 16.09.2018
comment
Спасибо за четкий ответ. Мне нравится подход исторической версии, он позволяет реконструировать домен, в то же время сохраняя повсеместный язык. Мне интересно, как бы вы предложили справиться с отношениями к этому объекту: допустим, у моей сущности User есть метод name(), который извлекает VO, я не могу напечатать подсказку для любого VO без потенциальной ошибки типа. Однако добавление интерфейса для Usernames может показаться слишком сложным. - person Stratadox; 17.09.2018
comment
@ RobertBräutigam Некоторые люди предлагают иногда извлекать «правила» +1, как это отражено в части «Решить» в эта схема - person guillaume31; 17.09.2018
comment
Также здесь: блог. leifbattermann.de/2017/04/21/ - person guillaume31; 17.09.2018

Есть ли вообще место для ценностных объектов в доменах с источником событий?

да.

Есть ли более простой способ избежать таких проблем?

«Не делай этого».

Проблема, которую вы описываете, на самом деле связана с обменом сообщениями - если мы внесем обратно несовместимые изменения в наши сообщения, все сломается.

(Точнее, у вас есть сообщение «Имя пользователя», и вы пытаетесь повторно использовать это сообщение с новым набором ограничений, которые отклоняют некоторые ранее допустимые варианты использования сообщения).

Ответ заключается в том, что вы не вносите обратно несовместимых изменений - вместо этого вводите новые имена, которые соответствуют новым требованиям, и исключите старые.

Другими словами, добавление поддержки новых сообщений и удаление поддержки старых сообщений стали двумя отдельно управляемыми вариантами.

В книге Грега Янга Версионирование в системе с обработкой событий этой идее посвящено несколько глав. Кроме того, Рич Хики затрагивает эти важные идеи в большинстве своих выступлений - я бы предложил начать с Спецификация.

«Объект значения», означающий, что тип, который текущая реализация модели предметной области использует для перемещения информации, представляет собой отдельную заботу от сообщений. Структуры данных, которые мы используем в памяти, не обязательно должны быть связаны с нашими форматами сериализации.

Представление информации в сети отличается от представления информации в памяти, а это, в свою очередь, отличается от абстракций, которые управляют информацией в памяти.

Сложность состоит в том, что в начале проекта у вас есть наименьшее количество информации о том, когда различные представления будут расходиться.

person VoiceOfUnreason    schedule 16.09.2018
comment
Интересные ресурсы, обязательно их посмотрю! Одна вещь, которая меня немного беспокоит, - это влияние изменения имени объекта значения на вездесущий язык. Существуют ли общепринятые способы борьбы с такими изменениями в этом контексте? - person Stratadox; 17.09.2018

Мы решили это немного по-другому. Отделив общедоступный 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 год назад по-прежнему остается именем пользователя, даже если наши правила изменились.
person Alex Justus    schedule 30.10.2018
comment
Красиво и чисто - проверка сейчас (в данный момент), но повторное воспроизведение старых событий мы тоже действительны (в то время) - поэтому нет необходимости повторно проверять. Они уже СДЕЛАНЫ. - person Tony Trembath-Drake; 15.11.2018
comment
@ Грег Янг: Разве UserName не означает, что ограничения проверяются во время создания? Не просачивается ли агрегатное состояние в событие домена? Разве агрегатное состояние не должно быть отделено от события предметной области? - person Mohsen; 07.08.2019

Хотя я уже ответил, я считаю, что это интересная ситуация.

Я согласен с другими в том, что данные о событиях должны быть основаны на записях и, следовательно, не более чем контейнер данных, который можно использовать для восстановления агрегата.

При этом, когда меняются правила, меняется и домен. Основная часть проектирования, ориентированного на предметную область, заключается в том, чтобы охватить как можно большую часть предметной области (правил / структуры). Если это так, следует ли не сохранять изменения в правилах?

Например, если у нас есть 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;
    }
}

Теперь мы переходим к 1 марта 2018 года, и правила меняются. Мы можем сохранить правило:

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;
    }
}

Это основная идея. Таким образом, мы сохраняем и наши «старые» правила. Это может доставить немало хлопот, но у меня недостаточно опыта, чтобы сказать об этом. Изменение наших правил задним числом может привести к довольно сложной ситуации, поэтому я думаю, что нужно будет оценивать это в каждом конкретном случае.

Просто мысль.

person Eben Roux    schedule 17.09.2018
comment
Спасибо, что поделились своим мнением по этому поводу! Это интересное решение - сохранить оба обстоятельства валидации в объекте значения. Я не совсем уверен, нравится ли мне, что нашему Username теперь нужно знать о RegistrationDate (раньше это было просто имя ...), но это определенно решает проблему обратной силы правил проверки. - person Stratadox; 17.09.2018