Навигация DDD к сущностям внутри совокупного корня через составное удостоверение

У меня есть общий корень Products, который содержит список объектов Selection, который, в свою очередь, содержит список объектов, называемых Features.

  • Агрегированный корень Product имеет идентификатор только имени
  • Объект Selection имеет идентификатор имени (и соответствующий ему Product identity)
  • Объект Feature имеет идентификатор имени (а также соответствующий ему идентификатор выбора)

Если идентификаторы для объектов построены следующим образом:

var productId = new ProductId("dedisvr");
var selectionId = new SelectionId("os",productId);
var featureId = new FeatureId("windowsstd",selectionId);

Обратите внимание, что зависимое тождество принимает идентификатор родителя как часть составного элемента.

Идея состоит в том, что это сформировало бы номер детали продукта, который может быть идентифицирован определенной особенностью в выборе, т.е. ToString() для указанного выше объекта featureId вернет dedisvr-os-windowsstd.

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

При запросе продукта для связанных функций объект Feature возвращается, но ключевое слово С# internal используется для скрытия любых методов, которые могут мутировать сущность, и, таким образом, гарантировать, что объект неизменен для службы вызывающего приложения (в другая сборка из кода домена).

Эти два вышеприведенных утверждения предусмотрены двумя функциями:

class Product
{
    /* snip a load of other code */

    public void AddFeature(FeatureIdentity identity, string description, string specification, Prices prices)
    {
       // snip...
    }

    public IEnumerable<Feature> GetFeaturesMemberOf(SelectionIdentity identity);
    {
       // snip...
    }
}

У меня есть агрегированный корень, называемый порядком обслуживания, он будет содержать ConfigurationLine, который будет ссылаться на Feature внутри корня агрегата Product на FeatureId. Это может быть в совершенно другом ограниченном контексте.

Так как FeatureId содержит поля SelectionId и ProductId, я буду знать, как перейти к этой функции через общий корень.

Мои вопросы:

Составные тождества, образованные с идентичностью родителя - хорошая или плохая практика?

В другом примере кода DDD, где идентификаторы определяются как классы, я еще не видел никаких композитов, сформированных из идентификатора локального объекта и его родительского идентификатора. Я думаю, что это приятное свойство, так как мы всегда можем перейти к этому объекту (всегда через общий корень) со знанием пути к нему (Product → Selection → Feature).

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

Ссылки на внутренние сущности - переходные или долгосрочные?

bluebook упоминает ссылки на сущности внутри агрегата, допустимы, но должны быть только временными (внутри кодового блока). В моем случае мне нужно хранить ссылки на эти сущности для использования в будущем, хранение не является временным.

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

Я считаю, что это правильно, и если да, то почему он упоминается, что привязка дочерних сущностей преходяще?

Исходный код ниже:

public class ProductIdentity : IEquatable<ProductIdentity>
{
    readonly string name;

    public ProductIdentity(string name)
    {
        this.name = name;
    }

    public bool Equals(ProductIdentity other)
    {
        return this.name.Equals(other.name);
    }

    public string Name
    {
        get { return this.name; }
    }

    public override int GetHashCode()
    {
        return this.name.GetHashCode();
    }

    public SelectionIdentity NewSelectionIdentity(string name)
    {
        return new SelectionIdentity(name, this);
    }

    public override string ToString()
    {
        return this.name;
    }
}

public class SelectionIdentity : IEquatable<SelectionIdentity>
{
    readonly string name;
    readonly ProductIdentity productIdentity;

    public SelectionIdentity(string name, ProductIdentity productIdentity)
    {
        this.productIdentity = productIdentity;
        this.name = name;
    }

    public bool Equals(SelectionIdentity other)
    {
        return (this.name == other.name) && (this.productIdentity == other.productIdentity);
    }

    public override int GetHashCode()
    {
        return this.name.GetHashCode();
    }

    public override string ToString()
    {
        return this.productIdentity.ToString() + "-" + this.name;
    }

    public FeatureIdentity NewFeatureIdentity(string name)
    {
        return new FeatureIdentity(name, this);
    }
}

public class FeatureIdentity : IEquatable<FeatureIdentity>
{
    readonly SelectionIdentity selection;
    readonly string name;

    public FeatureIdentity(string name, SelectionIdentity selection)
    {
        this.selection = selection;
        this.name = name;
    }

    public bool BelongsTo(SelectionIdentity other)
    {
        return this.selection.Equals(other);
    }

    public bool Equals(FeatureIdentity other)
    {
        return this.selection.Equals(other.selection) && this.name == other.name;
    }

    public SelectionIdentity SelectionId
    {
        get { return this.selection; }
    }

    public string Name
    {
        get { return this.name; }
    }

    public override int GetHashCode()
    {
        return this.name.GetHashCode();
    }

    public override string ToString()
    {
        return this.SelectionId.ToString() + "-" + this.name; 
    }
}

Ответ 1

Составные тождества, образованные с идентичностью родителя - хорошая или плохая практика?

Это хорошая практика, когда они используются должным образом: когда эксперт домена идентифицирует вещи локально (например, "Джон от маркетинга" ), они верны, в то время как они неверны.

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

Иногда вы сталкиваетесь с глобально идентифицированным сущностью (например, "Джон Смит" ), локально идентифицируемой экспертом, когда он говорит об определенном ограниченном контексте. В этих случаях требования BC выигрывают.
Обратите внимание, что это означает, что вам понадобится служба домена для сопоставления идентификаторов между BC, в противном случае все, что вам нужно, - общие идентификаторы.

Ссылки на внутренние сущности - переходные или долгосрочные?

Если агрегированный корень (в вашем случае Product) требует, чтобы дочерние сущности обеспечивали бизнес-инварианты, ссылки должны быть "долгосрочными", по крайней мере, до тех пор, пока не будут сохранены инварианты.

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

Объект Value неизменен только потому, что он не имеет идентификатора, а не наоборот.

Но когда вы говорите:

Однако необходимость сохранения этой ссылки предназначена только для отчетов и поиска

Я бы предложил вам использовать прямые SQL-запросы (или объект-запрос с DTO, или все, что у вас может быть для дешевых), вместо объектов домена. Отчеты и поиск не изменяют состояние объектов, поэтому вам не нужно сохранять инварианты. Это основное обоснование CQRS, которое просто означает: "используйте модель домена только тогда, когда вам нужно обеспечить бизнес-инварианты! Используйте WTF для компонентов, которые просто нужно читать!"

Дополнительные заметки

При запросе продукта для связанных функций объект Feature возвращается, но внутреннее ключевое слово С# используется для скрытия любых методов, которые могут мутировать объект...

Модификаторы доступа для обработки модификаторов в этом контексте являются дешевым подходом, если вам не нужны клиенты unit test, но если вам нужно протестировать код клиента (или ввести перехватчик AOP или что-то еще), простые старые интерфейсы являются лучшим решением.

Кто-то скажет вам, что вы используете "ненужную абстракцию", но с использованием ключевого слова языка (interface) не означает введения абстракций вообще!
Я не совсем уверен, что они действительно понимают что абстракция , настолько, что они путают инструменты (несколько ключевых слов языка, которые общий в OO) для акта абстрагирования.

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

Являются ли классы sealed конкретными? Являются ли struct бетоном? НЕТ!!!
Вы не можете бросить их, чтобы вредить некомпетентным программистам!
Они настолько абстрактны, как классы interface или abstract.

Абстракция бесполезна (хуже того, она опасна!), если она делает прозу кода непрочитанной, трудно следовать и т.д. Но, поверьте, он может быть закодирован как sealed class!

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

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

Для меня я решаю эту проблему, возвращая (а также используя внутренние) локальные сущности, которые на самом деле неизменяемы, заставляя клиентов хранить только ссылку на общий корень (также известный как основной объект) и подписаться на него.

Ответ 2

Составные тождества, образованные с идентичностью родителя - хорошая или плохая практика?

ИМХО, нет оснований полагать, что это плохая практика, если идентификатор объекта уникален в совокупном корне, он не имеет никакого значения, если идентификатор объекта является составным или даже уникальным вне корня агрегата. Единственным возражением может быть то, что эти составные идентификаторы отличаются от идентификаторов, используемых в словаре вашего домена, "вездесущий язык".

Ссылки на внутренние сущности - переходные или долгосрочные?

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

Ответ 3

Не полностью связан с вопросом, но я хотел бы начать с упоминания, что я не считаю интерфейс привлекательным. Кажется, вы разоблачаете класс Feature в одностороннем порядке. Либо разоблачите его, либо нет. Я не разработчик С#, поэтому, пожалуйста, не возражайте, если я сделаю синтаксические ошибки. Чтобы продемонстрировать, что я имею в виду:

Это предоставляет атрибуты Feature. Когда эти атрибуты изменяются, в любой усадьбе этот интерфейс также необходимо изменить.

public void AddFeature(FeatureIdentity identity, string description,
                       string specification, Prices prices)

Вы можете рассмотреть возможность принятия объекта Feature в качестве аргумента:

public void AddFeature(Feature feature)

Это способ очистки IMO.

По теме; ваш вопрос напоминает мне проекты NoSQL. Я как бы знаком с этими людьми, поэтому я могу быть предвзятым и, возможно, не хватает смысла.

Составление дочернего идентификатора с родительским идентификатором возможно несколькими способами и может быть или не быть плохой практикой. Подумайте, как будут доступны ваши объекты. Если вы только получаете доступ к дочерним объектам из родителя, имеет смысл объединить их. Если дочерние сущности могут быть также корневыми объектами, вам нужно будет обратиться к ним. Вы уже сделали это решение в своем вопросе.

не существует смысла для существования функции без выбора и выбора без связанного с ней продукта.

Было бы разумно, что ваш класс Product имеет коллекцию некоторого типа, содержащую объекты Selection. Класс Selection имел бы коллекцию, содержащую объекты Feature. Обратите внимание, что это может сделать объект Product очень тяжелым с точки зрения настойчивости, если в нем много объектов Selection, у которых может быть много объектов Feature. В таких случаях вам может быть лучше, если у вас есть ссылки по идентификатору.

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

Скомпилированный идентификатор имеет корни с SQL, я думаю, я видел код, который использует классы для моделирования этих тождеств. Мне нравится думать, что это скорее ограничение рамки персистентности или языка. Это просто конец для оправдания средств. Когда структура постоянных действий несколько заставляет вас сделать это, это приемлемо.

Ссылка на внутренние объекты звучит как нечто, чего вы не должны делать. В примере Product, Selection и Feature; нет смысла иметь выбор без продукта. Поэтому было бы более полезно обратиться к продукту. Для отчетности вы можете рассмотреть возможность дублирования данных. Это на самом деле обычная техника в NoSQL. Особенно, когда объекты неизменяемы, вы хотите рассмотреть возможность дублирования этих объектов в другом месте. Обращение к сущности приведет к другой операции "get-the-entity-operation", тогда как данные никогда не изменятся, это совершенно бессмысленно, если можно так сказать.

Ссылаясь на родителя или ребенка, это совсем не плохая практика. Эти отношения соблюдаются, это часть моделирования, а не то, что сущность существует без родителя. Если вы хотите заставить дочерний объект иметь родителя; требуется родительский элемент в дочернем конструкторе. Не используйте в родительском элементе метод create-child. Как я сказал выше, это усложнит ситуацию. Я бы лично не принуждал родителя, создав ребенка, который вы сами установили родитель.