DDD-навигация к сущностям внутри совокупного корня через составную идентификацию
У меня есть совокупный корень 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, он будет содержать ConfigurationLine, который будет ссылаться на Feature
в пределах Product
объединить корень FeatureId
, Это может быть в совершенно другом ограниченном контексте.
Поскольку FeatureId содержит поля SelectionId
а также ProductId
Я буду знать, как перейти к функции через сводный корень.
Мои вопросы:
Составные идентичности, сформированные с идентичностью родителя - хорошая или плохая практика?
В другом примере кода DDD, где идентификаторы определены как классы, я еще не видел каких-либо композитов, сформированных из локального идентификатора объекта и его родительского идентификатора. Я думаю, что это хорошее свойство, так как мы всегда можем перейти к этой сущности (всегда через корень совокупности), зная путь к нему (Product -> Selection -> Feature).
Хотя мой код с составной цепочкой идентификаторов с родителем имеет смысл и позволяет мне переходить к сущности через корневую совокупность, не видя других примеров кода, где идентичности формируются аналогично с композитами, я очень нервничаю - любая причина этого или это плохая практика?
Ссылки на внутренние организации - временные или долгосрочные?
В справочнике упоминаются ссылки на объекты в совокупности, которые приемлемы, но должны быть только временными (в пределах блока кода). В моем случае мне нужно хранить ссылки на эти объекты для использования в будущем, хранение не является временным.
Тем не менее, необходимость хранить эту ссылку предназначена только для отчетов и поиска, и даже если я действительно хочу получить дочернюю сущность с помощью корня, возвращаемые сущности являются неизменяемыми, поэтому я не вижу никакого вреда или инвариантов. сломана.
Правильно ли мое мышление, и если да, то почему оно упоминается так, чтобы ссылки на дочерние сущности были временными?
Исходный код ниже:
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, в противном случае все, что вам нужно, это общие идентификаторы.
Ссылки на внутренние организации - временные или долгосрочные?
Если совокупный корень (в вашем случае Product
) требует, чтобы дочерние объекты обеспечивали бизнес-инварианты, ссылки должны быть "долгосрочными", по крайней мере, до тех пор, пока эти инварианты не должны храниться.
Более того, вы правильно поняли обоснование внутренних сущностей: они являются сущностями, если их идентифицирует эксперт, изменчивость - это проблема программирования (а неизменность всегда безопаснее). Вы можете иметь неизменяемые сущности, локальные для других или нет, но то, что делает их сущностями, заключается в том, что эксперт идентифицирует их, а не в их неизменности.
Значения объекта неизменны только потому, что они не имеют идентичности, а не наоборот!
Но когда вы говорите:
Однако эта ссылка должна храниться только для отчетов и поиска.
Я бы посоветовал вам использовать прямые SQL-запросы (или объекты запроса с DTO, или все, что вы можете иметь по дешевке) вместо объектов домена. Отчеты и поиск не изменяют состояние сущностей, поэтому вам не нужно сохранять инварианты. Это основное обоснование CQRS, которое просто означает: "используйте модель предметной области только тогда, когда вам нужно обеспечить бизнес-инварианты! Используйте WTF, который вам нравится, для компонентов, которые просто необходимо прочитать!"
Дополнительные заметки
При запросе продукта о связанных функциях возвращается объект Feature, но используется внутреннее ключевое слово C#, чтобы скрыть любые методы, которые могут изменить объект...
Модификаторы доступа для обработки модификаторов в этом контексте - дешевый подход, если вам не нужно тестировать клиентские модули, но если вам нужно протестировать клиентский код (или ввести перехватчик AOP, или что-то еще), простые старые интерфейсы - лучшее решение,
Кто-то скажет вам, что вы используете "ненужную абстракцию", но используете ключевое слово языка (interface
) вовсе не означает введение абстракций!
Я не совсем уверен, что они действительно понимают, что такое абстракция, настолько, что они путают инструменты (несколько языковых ключевых слов, которые часто встречаются в ОО) для абстрагирования.
Абстракции находятся в уме программиста (и в уме эксперта, в DDD), код просто выражает их через конструкции, предоставляемые языком, который вы используете.
Являются sealed
классы бетонные? Являются struct
бетон? НЕТ!!!
Вы не можете бросить их, чтобы навредить некомпетентным программистам!
Они так же абстрактны, как и interface
с или abstract
классы.
Абстракция не нужна (что еще хуже, она опасна!), Если она делает не понятной прозу кода, трудной для понимания и так далее. Но, поверьте мне, это можно закодировать как sealed class
!
... и, таким образом, убедитесь, что объект является неизменным для вызывающей службы приложений (в сборке, отличной от кода домена).
ИМХО, вам также следует учитывать, что если "по-видимому неизменные" локальные сущности, возвращаемые агрегатом, могут фактически изменить часть своего состояния, клиенты, которые их получили, не смогут знать, что такое изменение произошло.
Для меня, я решаю эту проблему, возвращая (а также используя внутри себя) локальные сущности, которые на самом деле являются неизменяемыми, заставляя клиентов хранить только ссылку на совокупный корень (он же основной объект) и подписывать события на него.
Составные идентичности, сформированные с идентичностью родителя - хорошая или плохая практика?
ИМХО, нет никаких оснований полагать, что это плохая практика, поскольку идентификатор сущности является уникальным в пределах совокупного корня, не имеет значения, является ли идентификатор сущности составным или даже уникальным вне совокупного корня. Единственное возражение, которое может возникнуть, состоит в том, что эти составные идентификаторы отличаются от идентификаторов, используемых в словаре вашего домена, "вездесущем языке".
Ссылки на внутренние организации - временные или долгосрочные?
Если эти объекты являются неизменяемыми, эти объекты должны были быть смоделированы как объекты значений. В противном случае, непосредственно ссылаясь на эти сущности, вы рискуете получить доступ к сущности, которая больше не связана с данным совокупным корнем или была изменена в это время.
Не совсем связанный с вопросом, но я хотел бы начать с упоминания, что я не нахожу интерфейс привлекательным. Кажется, будто вы подвергаете Feature
класс односторонний. Либо разоблачить это, либо нет. Я не разработчик C#, поэтому, пожалуйста, не возражайте, если я сделаю какие-либо синтаксические ошибки. Чтобы продемонстрировать, что я имею в виду:
Это раскрывает атрибуты функции. Когда эти атрибуты меняются, в любом поместье, этот интерфейс также должен быть изменен.
public void AddFeature(FeatureIdentity identity, string description,
string specification, Prices prices)
Вы можете рассмотреть возможность принятия Feature
объект в качестве аргумента:
public void AddFeature(Feature feature)
Это способ чище ИМО.
По теме; Ваш вопрос напоминает мне о дизайне NoSQL. Я в некотором роде знаком с ними, поэтому я могу быть предвзятым и, возможно, упустить смысл.
Составление дочернего идентификатора с родительским идентификатором возможно несколькими способами и может быть или не быть плохой практикой. Подумайте, как будут доступны ваши сущности. Если вы получаете доступ только к дочерним объектам из родительского объекта, имеет смысл объединиться. Если дочерние сущности также могут быть корневыми сущностями, вам нужно обратиться к ним. Вы уже приняли решение в своем вопросе.
не имеет смысла, чтобы функция существовала без выбора и выбора без связанного продукта.
Было бы понятно, что ваш Product
класс имеет коллекцию некоторого рода, содержащую Selection
объекты. Selection
класс будет иметь коллекцию, содержащую Feature
объекты. Обратите внимание, что это может сделать Product
объект очень тяжелый, с точки зрения постоянства, если он имеет много Selection
объекты, которые могут иметь много Feature
объекты. В таких случаях вам может быть лучше иметь их в качестве ссылок по идентификатору.
Идентификаторы, используемые в вашем коде, кроме уровня персистентности, не обязательно должны состоять как из дочерних, так и из родительских идентификаторов, поскольку они уже находятся в определенном контексте. Это может, однако, быть конструктивным решением для улучшения читабельности данных.
Я думаю, что составная идентичность имеет корни в SQL, я видела код, который использует классы для моделирования этих идентичностей. Мне нравится думать, что это скорее ограничение персистентной структуры или языка. Это всего лишь конец, чтобы оправдать средства. Когда структура постоянства несколько заставляет вас делать это, это приемлемо.
Ссылка на внутренние сущности звучит как то, что вы не должны делать. В примере Product
, Selection
а также Feature
; нет смысла иметь выбор без продукта. Так что было бы больше смысла ссылаться на продукт. Для отчетности вы можете рассмотреть возможность дублирования данных. Это на самом деле распространенная техника в NoSQL. Особенно, когда объекты являются неизменяемыми, вы хотите продублировать эти объекты в другом месте. Ссылка на сущность приведет к другой операции "получить сущность", в то время как данные никогда не изменятся, если можно так сказать, это совершенно бессмысленно.
Обращение к родителю или ребенку - совсем не плохая практика. Эти отношения навязаны, это часть моделирования, а не то, что сущность существует без родителя. Если вы хотите заставить дочернюю сущность иметь родителя; требует родителя в конструкторе ребенка. Пожалуйста, не реализуйте метод create-child в родительском. Как я уже говорил выше, это усложнит ситуацию. Я бы лично не заставлял иметь родителя, при создании ребенка вы сами устанавливаете родителя.