DDD-навигация к сущностям внутри aggregate root через составную идентификацию

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

  • совокупный корень Product имеет идентичность только имя
  • сущности Selection имеет идентификатор имени (и соответствующий идентификатор продукта)
  • сущности Feature имеет идентификатор имени (а также соответствующий идентификатор выбора)

где личность для сущностей строятся следующим образом:

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

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

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

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

при запросе продукта для связанных функций возвращается объект Feature, но C# 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...
    }
}

у меня есть агрегатный корень под названием Service order, это будет содержать строку конфигурации, которая будет ссылаться на Feature внутри Product совокупный корень FeatureId. Это может происходить в совершенно ином ограниченном контексте.

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

мои вопросы:

составные идентичности, сформированные с идентичностью родителя-хорошая или плохая практика?

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

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

ссылки на внутренние объекты - временные или долгосрочные?

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

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

правильно ли мое мышление, и если да, то почему упоминается, что ссылки на дочерние сущности преходящи?

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

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

3 ответов


составные идентичности, сформированные с идентичностью родителя-хорошая или плохая практика?

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

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

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

ссылки на внутренние объекты - временные или долгосрочные?

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

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

Value object неизменяемы только потому, что у них нет идентичности, а не наоборот!

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

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

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

дополнительные примечания

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

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

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

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

Are sealed классы бетона? Are structs бетон? нет!!!
Вы не можете бросить их, чтобы навредить некомпетентным программистам!
Они так же абстрактны, как interfaceили abstract классы.

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

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

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

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


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

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

ссылки на внутренние объекты - временные или долгосрочные?

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


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

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

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

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

public void AddFeature(Feature feature)

это путь чище ИМО.

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

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

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

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

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

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

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

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