Применение бизнес-правил из Aggregate

#c# #.net #domain-driven-design #cqrs #clean-architecture

Вопрос:

В настоящее время я изучаю подходы DDD и CQRS. Я начал проект, связанный с доменом ресторана.

Один из модулей, который я нашел (используя Штурм событий), — это Меню. В модуле Меню менеджер ресторана может хранить несколько меню. Меню-это агрегатор, который применяет правила между группами и связанными элементами (позициями) в меню.

Проблемные Правила:

  1. Одно меню в ресторане может быть активным одновременно.
  2. Внутренние названия меню должны быть уникальными в Ресторане.

Чтобы обеспечить соблюдение этих правил, я пытаюсь передать интерфейс репозитория ресторана в меню (я больше сосредоточился на 1. правиле, потому что 2. еще сложнее). Я чувствую, что делаю что-то не так. У меня была идея использовать доменную службу для обеспечения этого, и после анализа я не думаю, что это будет лучше. У вас есть какие-нибудь советы для меня? Спасибо 🙂

 public class Menu : AggregateRoot<MenuId>
{
    public string InternalName { get; private set; }
    
    public RestaurantId RestaurantId { get; private set; }

    public IReadOnlyList<Group> Groups => _groups;
    private List<Group> _groups = new();

    ...

    internal void Activate()
    {
        CheckRule(new ActiveMenuMustHaveAtLeastOneGroup(_groups));
        
        _groups.ForEach(x => x.CheckConsistency());
    }

    public void ChangeInternalName(string newInternalName, IRestaurantRepository 
         restaurantRepository)
    {
        CheckRule(new InternalNameMustBeUniqueInRestaurantMenusRule(RestaurantId, 
             newInternalName, restaurantRepository));
        InternalName = newInternalName;
    }
}

public class Restaurant : AggregateRoot<RestaurantId>
{
    public MenuId ActiveMenuId { get; private set; }

    public IReadOnlyList<MenuId> MenuIds => _menuIds;
    private List<MenuId> _menuIds = new();

    ...

    public void ChangeActiveMenu(MenuId newActiveMenuId, IMenuRepository menuRepository)
    {
        CheckMenuExists(newActiveMenuId);
        CheckRule(new CannotActivateActiveMenuRule(ActiveMenuId, newActiveMenuId));
        
        var menu = menuRepository.GetAsync(newActiveMenuId).Resu<
        menu.Activate();
        ActiveMenuId = newActiveMenuId;
    }

    ...
}
 

Ответ №1:

Хорошо, давайте попробуем разобраться с этим сверху — и, пожалуйста, имейте в виду, что мой ответ >основан на мнении><, как и большинство дизайнерских решений.

Для первого вопроса:
Это Restaurant работа по назначению активного меню, поэтому эта совокупность должна содержать соединение вошь, чтобы Menu … скажем, она хранит свой идентификатор. Только один идентификатор на то пошло.
При выборе меню активации, Restaurant АР можете легко проверить, если текущий идентификатор или нет, без необходимости репозитория.
Теперь, если данного меню идентификатор разный, то Restaurant должен быть способ сообщить Menu контексте изменения — это хорошо, что знает меню, если ее активный — это совсем другое дело.

Итак, вот CQR с обработчиками событий — Restaurant должны выдавать MenuChanged событие, и этот обработчик событий должен отвечать за обновление menu aggregate.
Поэтому вы должны запустить событие домена в своем ChangeActiveMenu методе, которое будет запущено при сохранении агрегата (обычно хранилище выполняет сохранение).

     public void ChangeActiveMenu(MenuId newActiveMenuId)
    {
        CheckRule(new CannotActivateActiveMenuRule(ActiveMenuId, newActiveMenuId));
        ActiveMenuId = newActiveMenuId;
        this.EventStore.Add(new MenuChangedEvent(newActiveMenuId));
    }
 

Теперь работа по активации меню переходит к MenuChangedEvent обработчику — часто это выполняется с помощью популярной библиотеки MediatR.

Второй вопрос: Возможно множество способов проверки уникальности имени по совокупным корням.
Одно можно сказать наверняка — aggregate не должен заставлять инварианты выходить за его пределы. Таким образом, в данном случае инвариантом является a Name , и пересечение границы позволит Menu проверить согласованность между несколькими совокупными экземплярами.

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

Не забывайте и / или делегат — если вы используете реляционную базу данных, то почему бы не реализовать составной ключ, в данном случае Menu сделаны из RestaurantId и Name — тогда инвариантность будет также вынужден дБ.
Вам уникальных сдерживать исключение — совокупности не сохраняется, события ее не отправили и т. д. и вам просто нужно справиться с этим где-то.

Это было бы моим решением goto для этого, а не просто расширением Restaurant агрегата, чтобы содержать Menu его как часть.

Опять же, это основано на мнении, и существует много, много способов решения этой проблемы.

Комментарии:

1. Спасибо вам за ваш длинный и исчерпывающий ответ. 1. Подход, основанный на магазине событий. Чтобы активировать Меню, Меню должно соответствовать другим бизнес-правилам: например, должно иметь хотя бы одну группу (Неактивное меню может быть пустым). EventStore-это подход, основанный на транзакциях, верно? Значит, если мы нарушим правила в меню, это вернет изменение ActiveMenuId в ресторане? 2. Доменная служба. Поэтому, пытаясь изменить внутреннее имя в меню, я должен обратиться к службе домена. В этом случае метод ChangeInternalName может иметь внутренний модификатор доступа?

2. 1 — меню должно отвечать за правильность своего состояния — я бы сказал, что оно должно проверять, что у него есть хотя бы одна группа самостоятельно — добавлять и удалять группы общедоступными методами, которые будут проверять это. Создайте агрегат меню также с принудительным применением этого правила, и проблем не возникнет (принудительно создайте хотя бы одну группу в конструкторе). 2 — Что касается изменения имени — оставьте его общедоступным и принудительно измените уникальность на другом шаге, используя доменную службу перед сохранением и / или при сохранении. Если что-то не так, aggregate не будет сохранен, и вы исправите ошибку.

3. Таким образом, поток выше будет помещен в какой-нибудь обработчик команд cqrs, например CreateMenuCommand . И, проверяя при сохранении, я имею в виду, что вы можете обработать исключение базы данных, если использовался составной ключ из ответа.