Может ли агрегатный инвариант включать правило, основанное на информации из других источников?

Может ли инвариант агрегатов в DDD включать правило, основанное на информации из другого агрегата? Теперь я так не думаю, однако это вызывает у меня проблему, и я не знаю, как ее решить.

У меня есть объект под названием Актив (оборудование), который я моделирую как корень агрегата. Он имеет список тегов (свойств), которые описывают такие вещи, как производитель, модель и т. д. Он хранит идентификатор второго агрегата, называемого AssetType, который имеет список типов тегов, некоторые из которых могут быть помечены как обязательные.

Теперь мне кажется, что одно из неизменяемых условий для Asset должно ссылаться на связанный AssetType, чтобы обеспечить ненулевые значения в списке обязательных тегов. Но мои кишки сжимаются от мысли о том, как я буду добиваться постоянства.

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

И AssetType также не может очень хорошо входить в совокупность активов. Это кажется таким же абсурдным.

Моя интуиция по-прежнему говорит, что Asset и AssetType — это два отдельных агрегата, но как решить проблемы согласованности? Или я ошибся в своем инварианте?


person MJM    schedule 05.04.2013    source источник


Ответы (2)


В этом сценарии есть несколько способов применения инвариантов.

Прежде всего, рассмотрите поведение агрегата Asset. Я предполагаю, что есть как минимум CreateAssetCommand и RemoveTagCommand. Инварианты должны применяться во время выполнения этих команд следующим образом:

СоздатьКомандуАссета

Поскольку актив всегда связан с типом актива, в этой команде необходимо указать AssetTypeId. Этот идентификатор должен быть получен вызывающим абонентом, возможно, путем поиска определенного типа актива. При поиске AssetType можно также получить соответствующие TagType объекты, в частности обязательные. Это позволит вызывающему объекту создать необходимые экземпляры Tag для отправки как часть команды. Обратите внимание, что вызывающая сторона обязана предоставить допустимый тип актива и теги.

Команда удаления тегов

Обработчик этой команды может получить соответствующий файл Asset, в котором хранится файл AssetTypeId. Затем обработчик извлекает набор обязательных тегов для типа ресурса и гарантирует, что эти теги не будут удалены. В этом случае инвариант применяется самим обработчиком.

Другой способ обработки этих инвариантов – ввести согласованность в конечном счете, если это приемлемо. При таком подходе удаление тега из актива должно привести к публикации файла TagRemovedEvent. Затем обработчик этого события может проверить, не был ли удален обязательный тег. Если это так, он может создать задачу или уведомление о том, что актив находится в недопустимом состоянии. Обратите внимание: это предполагает, что актив может находиться в недопустимом состоянии, пока что-то не будет исправлено.

Теперь о поведении вокруг AssetType. Одной из команд, которая может поставить под угрозу целостность агрегата Asset, является введение нового обязательного Tag. В этом случае единственным способом обеспечения целостности является создание соответствующих тегов для каждого соответствующего актива. Поскольку это, скорее всего, не может быть сделано автоматически, конечная согласованность должна быть принята до тех пор, пока соответствующие теги не будут предоставлены вручную.

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

Подробнее об этом читайте в статье Эффективный совокупный дизайн.

person eulerfx    schedule 05.04.2013
comment
Спасибо за ответ. Я думаю, что возможная согласованность - это ответ, как вы сказали. Хотя @GiacomoTesio также оказал большую помощь - person MJM; 15.04.2013
comment
Я также хотел бы указать другим, читающим это, что инстинкт делать выводы об условиях слишком прост. Как мы обнаружили, фактические факты таковы, что активы создаются из AssteTypes, но затем более или менее отделяются от исходного типа. Ссылка может быть возвращена в любой момент времени к исходной спецификации Assets и в результате обновлена ​​в некотором смысле, но это действие вызывается вызывающей стороной, а не как побочный эффект сохранения исходного типа. Это осознание очень помогло. - person MJM; 15.04.2013

Может ли агрегатный инвариант включать правило, основанное на информации из других источников?

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

Кто-то использует для доступа к аппликативным сервисам через синглтоны, локаторы сервисов и так далее, но, по-моему, это запах тесно связанных приложений. Они забывают, что аргументы методов являются эффективными инъекторами зависимостей! :-)

Может ли инвариант агрегатов в DDD включать правило, основанное на информации из другого агрегата?

Нет.
За исключением случаев, когда второй агрегат предоставляется через аргументы команд, конечно.

ВНИМАНИЕ! ! !

У меня есть объект с именем Asset (оборудование)...
... (и) второй агрегат с именем AssetType...

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

Скорее всего, вы выбираете неправильные абстракции.

я неправильно понял свой инвариант?

Вероятно... Вы обращались к эксперту домена? он говорит о "TagTypes"?

Вы никогда не должны абстрагироваться самостоятельно.

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

ОТВЕТ

Если (и только если) эксперт в предметной области действительно описал модель в этих терминах, возможен следующий подход:

  1. вы можете создать класс AssetType с фабричным методом, который превращает IEnumerable<Tag> в TagSet language.html" rel="nofollow">и выдает либо MissingMandatoryTagException, либо UnexpectedTagException, если часть тега отсутствует или непредвиденна.
  2. в классе Asset команда RegisterTags будет принимать AssetType и IEnumerable<Tag>, выдавая MissingMandatoryTagException и WrongAssetTypeException (обратите внимание, насколько важны исключения для обеспечения инвариантов).

редактировать
что-то вроде этого, но гораздо более документированного:

public class AssetType
{
    private readonly Dictionary<TagType, bool> _tagTypes = new Dictionary<TagType, bool>();
    public AssetType(AssetTypeName name)
    {
        // validation here... 
        Name = name;
    }

    /// <summary>
    /// Enable a tag type to be assigned to asset of this type.
    /// </summary>
    /// <param name="type"></param>
    public void EnableTagType(TagType type)
    {
        // validation here... 
        _tagTypes[type] = false;
    }

    /// <summary>
    /// Requires that a tag type is defined for any asset of this type.
    /// </summary>
    /// <param name="type"></param>
    public void RequireTagType(TagType type)
    {
        // validation here... 
        _tagTypes[type] = false;
    }

    public AssetTypeName Name { get; private set; }


    /// <summary>
    /// Builds the tag set.
    /// </summary>
    /// <param name="tags">The tags.</param>
    /// <returns>A set of tags for the current asset type.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="tags"/> is <c>null</c> or empty.</exception>
    /// <exception cref="MissingMandatoryTagException">At least one of tags required 
    /// by the current asset type is missing in <paramref name="tags"/>.</exception>
    /// <exception cref="UnexpectedTagException">At least one of the <paramref name="tags"/> 
    /// is not allowed for the current asset type.</exception>
    /// <seealso cref="RequireTagType"/>
    public TagSet BuildTagSet(IEnumerable<Tag> tags)
    {
        if (null == tags || tags.Count() == 0)
            throw new ArgumentNullException("tags");
        TagSet tagSet = new TagSet();

        foreach (Tag tag in tags)
        {
            if(!_tagTypes.ContainsKey(tag.Key))
            {
                string message = string.Format("Cannot use tag {0} in asset type {1}.", tag.Key, Name);
                throw new UnexpectedTagException("tags", tag.Key, message);
            }
            tagSet.Add(tag);
        }

        foreach (TagType tagType in _tagTypes.Where(kvp => kvp.Value == true).Select(kvp => kvp.Key))
        {
            if(!tagSet.Any(t => t.Key.Equals(tagType)))
            {
                string message = string.Format("You must provide the tag {0} to asset of type {1}.", tagType, Name);
                throw new MissingMandatoryTagException("tags", tagType, message);
            }
        }

        return tagSet;
    }
}

public class Asset
{
    public Asset(AssetName name, AssetTypeName type)
    {
        // validation here... 
        Name = name;
        Type = type;
    }

    public TagSet Tags { get; private set; }

    public AssetName Name { get; private set; }

    public AssetTypeName Type { get; private set; }

    /// <summary>
    /// Registers the tags.
    /// </summary>
    /// <param name="tagType">Type of the tag.</param>
    /// <param name="tags">The tags.</param>
    /// <exception cref="ArgumentNullException"><paramref name="tagType"/> is <c>null</c> or
    /// <paramref name="tags"/> is either <c>null</c> or empty.</exception>
    /// <exception cref="WrongAssetTypeException"><paramref name="tagType"/> does not match
    /// the <see cref="Type"/> of the current asset.</exception>
    /// <exception cref="MissingMandatoryTagException">At least one of tags required 
    /// by the current asset type is missing in <paramref name="tags"/>.</exception>
    /// <exception cref="UnexpectedTagException">At least one of the <paramref name="tags"/> 
    /// is not allowed for the current asset type.</exception>
    public void RegisterTags(AssetType tagType, IEnumerable<Tag> tags)
    {
        if (null == tagType) throw new ArgumentNullException("tagType");
        if (!tagType.Name.Equals(Type))
        {
            string message = string.Format("The asset {0} has type {1}, thus it can not handle tags defined for assets of type {2}.", Name, Type, tagType.Name);
            throw new WrongAssetTypeException("tagType", tagType, message);
        }
        Tags = tagType.BuildTagSet(tags);
    }
}
person Giacomo Tesio    schedule 05.04.2013
comment
Ваше подозрение, что проблема в моделировании, верно. Мы потратили некоторое время (на самом деле много), выясняя, что же на самом деле происходит. Я думаю, что решение «конечная согласованность» является лучшим. Когда они решают, что хотят перечислить новую часть информации о Типе, они фактически не предоставляют информацию на месте всем Активам. Таким образом, метод «RebuildTags» в Asset будет считывать AssetType, а затем при необходимости аннулировать для каждого актива (а также, возможно, DetectTagErrors). Ваш ответ превосходен и подчеркнул это, хотя фактический ответ есть у @eulerfx. - person MJM; 15.04.2013