Стратегия выбора правильного Агрегированного корня, чтобы транзакция не охватывала несколько агрегатов - PullRequest
1 голос
/ 28 апреля 2019

Я моделирую контекст домена категории / меню и решил иметь 2 совокупных корня для этого контекста.

 public class MenuItem : Aggregate<Guid>
{
    public List<string> ImageUrls { get; set; }
    public decimal Price { get; set; }
    public IList<ExtraProperty> Extras { get; set; }
    public ITranslationList<MenuItemTranslation> Translations { get; set; }
    public bool Active { get; set; }
}




 public class Category : Aggregate<Guid>
 {
    public ITranslationList<CategoryTranslation> Translations { get; set;}
     public SortedList<int,Guid> Children { get; set; }
    public List<string> ImageUrls { get; set; }

    internal Category() { }


}

В модели категории свойство Children представляет собой отсортированный список дочерних идентификаторов категории и MenuItems.

Теперь предположим, что я хочу создать категорию.У меня есть команда для этой цели:

  public class CreateCategoryCommand:ICommand
  {
    public Guid Id { get; set; }
    public List<string> ImageUrls { get; set; }
    public ITranslationList<CategoryTranslation> Translations  { get; set; }
    public Guid UserId { get; set; }
    public Guid? ParentId { get; set; }
    public int ParentSortIndex { get; set; }
  }

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

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

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

Какова будет ваша стратегия / предложения / мнения для моделирования этого контекста?

1 Ответ

1 голос
/ 28 апреля 2019

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

Если вы измените направление ссылки от дочернего к родительскому, удалив Children свойство и добавление свойства ParentID это решит вашу проблему границы согласованности.Добавление новых категорий не повлияет на родителя.

Вы можете добавить методы GetChildren (parentID) или GetChildrenIDs (parentID) к CategoryRepository для получения детей или их идентификаторов Category , если они необходимы.

Редактировать:

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

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

Давайте зададим пару вопросов о порядке заказа Категории .

  • Вопрос 1: Как ParentSortIndex рассчитывается из отправителя Command , чтобы его можно было установить в Команда ?

  • Вопрос 2: Если Категория не имеет детей, допустимо ли получить Команда с ParentSortIndex = 10?

  • Вопрос 3: Значение ParentSortIndex важноили упорядочение категорий - единственное, что имеет значение?

Предположим, что упорядочение Categories - единственное, что имеет значение, и как оно реализуется, илизначение SortIndex не имеет значения.

Сначала давайте введем понятие SortingIndex .Теперь давайте подумаем о реализации этой концепции.Мы можем использовать float в качестве значения SortingIndex вместо int (или удвоить, если мы ожидаем много Categories ).У поплавков есть замечательное свойство, которое вы можете (почти) всегда найти между двумя другими поплавками.Например, если у вас есть 1 и 2, между ними 1,5, а между 1,2 - 1 и т. Д.

Далее давайте добавим CategoryRepository.GetSortingIndicesForChilren (parentId) метод.Этот метод получит объект со свойствами для CategoryGuid и SortingIndex для всех дочерних элементов родителя, чтобы мы могли вычислить SortingIndex , который находится рядом с запрошенной Category .

Это позволит избежать необходимости загружать всех детей.Получение специальных значений, возвращаемых из Reposistories - хорошая техника.В DDD book Эрик Эванс объясняет это и говорит, что для Repositories вполне нормально возвращать такие специальные объекты, которые содержат некоторую информацию или данные.

Далее давайте уточнимв какой дочерний элемент мы хотим поместить новую дочернюю категорию вместо указания конкретного значения индекса.(Возможно, мы захотим поместить его выше категории ниже, но для простоты я пропущу этот случай. Он может быть решен с помощью enum {placeAbove, placeBellow}, который можно добавить к Command )

public class SortingIndex : ValueObject {

  public static readonly MinValue = new SotringIndex(float.MinValue);
  public static readonly MidValue = new SotringIndex(float.MaxValue);
  public static readonly MaxValue = new SotringIndex(float.MaxValue);

  public float Value { get; private set; }

  public SortingIndex(float value) { .... }

  public SortingIndex GetBtween(SortingIndex other) { ... }

  public static operator > (OrderingPriority other) { .. }
  public static operator >= (OrderingPriority other) { .. }
  // other operators <=, ==, != etc.
}

public class Category : Aggregate<Guid> {
   public Guid ParentGuid { get; private set; }
   public SortingIndex SortingIndex { get; private set; }
   // constructor and other stuff......
}

public class CreateCategoryCommand : ICommand
{
    public Guid? ParentId { get; set; }
    public Guid? CategoryGuidToPlaceNextTo { get; set; }
    // other stuff...
}

public class CreateCategoryCommandHandler {

  public void Handle(CreateCategoryCommand cmd) {

    var sortingIndex = SortingIndex.MidValue;

    // start with mid value. If there aren't any children, this will be the 
       first. Later when we add other children we can calculate an index 
       before of after this one.

    if(cmd.ParentID != null && cmd.CategoryGuidToPlaceNextTo != null) {

          var childrenSortingIndices = CategoryRepository
                           .GetSortingIndicesForChilren(cmd.ParentID);

           sortingIndex = PlaceChildNextTo(
                            childrenSortingIndices,
                            cmd.CategoryGuidToPlaceNextTo);
     }

    var category = new Category(cmd.ID, cmd.ParentID, sortingIndex, ...);

    CategoryRepository.Save(category);
  }
}

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

Наличие коллекции с детьми вызовет мутацию состояния в этой коллекции.

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

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

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

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

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

...