Введение в CQRS + Event Sourcing: Часть 2

    В прошлой статье я начал с основ CQRS + Event Sourcing. В этот раз я предлагаю продолжить и более подробно посмотреть на ES.

    В примере который я выкладывал с моей прошлой статьей магия Event Sourcing’а была скрыта за абстракцией IRepository и двумя методами IRepository.Save() и IRepository.GetById<>().
    Для того чтобы поподробнее разобраться что происходит я решил рассказать о процессе сохранения и загрузки агрегата из Event Store на примере проекта Lokad IDDD Sample от Рината Абдулина. У него в аппликейшен сервисах идет прямое обращение к Event Store, без дополнительных абстракций, поэтому все выглядит очень наглядно. Application Service — это аналог CommandHandler, но который обрабатывает все комманды одного агрегата. Такой подход очень удобный и мы тоже на него в одном проекте перешли.

    ApplicationService


    Интерфейс IApplicationService крайне прост.
    public interface IApplicationService
    {
       void Execute(ICommand cmd);
    }
    

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

    Так как у Рината в примере только один аггрегат Customer, то и сервис тоже только один CustomerApplicationService. Собственно поэтому нет необходимости выносить какую-либо логику в базовый класс. Отличное решение для примера на мой взгляд.

    Метод Execute передает обработку комманды одной из перегрузок метода When подходящей по сигнатуре. Реализация метода Execute очень простая с использованием динамиков, и не надо бежать рефлексией по всем методам.
    public void Execute(ICommand cmd)
    {
       // pass command to a specific method named when
       // that can handle the command
       ((dynamic)this).When((dynamic)cmd);
    }
    

    Начнем с комманды создания — CreateCustomer.
    [Serializable]
    public sealed class CreateCustomer : ICommand
    {
       public CustomerId Id { get; set; }
       public string Name { get; set; }
       public Currency Currency { get; set; }
    
       public override string ToString()
       {
           return string.Format("Create {0} named '{1}' with {2}", Id, Name, Currency);
       }
    }
    

    В реальном проекте у вас между UI и ApplicationService скорее всего будет message queue, ну а для примера Ринат ограничился прямой передачей комманды объекту апликейшен сервиса (см. class ApplicationServer).
    После того, как команда CreateCustomer попадает в метод Execute, она перенаправляется в метод When.
    public void When(CreateCustomer c)
    {
       Update(c.Id, a => a.Create(c.Id,c.Name, c.Currency, _pricingService, DateTime.UtcNow));
    }
    

    В метод Update мы передаем айдишку агрегата и экшен который вызывает метод изменения состояния аггрегата. Вообще на мой взгляд лучше не делать метод Create у аггрегата, а создать еще один конструктор, так как вызов метода Create в данном контексте выглядит немного странным. Мы вроде и создаем агрегат, но почему-то метод Create передаем как метод изменения состояния. С конструктором так бы уже не получилось.

    Вернемся к методу Update, задача у него следующая — 1) загрузить все события для текущего аггрегата, 2) создать агрегат и восстановить его состояние использую загруженные события, 3) выполнить переданное действие Action execute над аггрегатом и 4) сохранить новые события если они есть.
    void Update(CustomerId id, Action<Customer> execute)
    {
       // Load event stream from the store
       EventStream stream = _eventStore.LoadEventStream(id);
       // create new Customer aggregate from the history
       Customer customer = new Customer(stream.Events);
       // execute delegated action
       execute(customer);
       // append resulting changes to the stream
       _eventStore.AppendToStream(id, stream.Version, customer.Changes);
    }
    


    Восстановление состояния


    В примере, который я показывал в прошлой статье состояние аггрегата хранилось в виде private полей в классе аггрегата. Это не совсем удобно если вы захотите добавить snapshot’ы, т.к. придется как-то высасывать состояние какждый раз или использовать рефлексию. У Рината гораздо более удобный подход — для состояние отдельный класс CustomerState, что позволяет вынести методы проекции из аггрегата и гораздо проще сохранять и загружать snapshot’ы, хоть их и нет в примере.
    Как видно, в конструктор агрегату передается список событий этого же аггрегата, как не сложно догадаться, для того чтобы он восстановил своё состояние.
    Агрегат в свою очередь делегирует восстановление состояние классу CustomerState.
    /// <summary>
    /// Aggregate state, which is separate from this class in order to ensure,
    /// that we modify it ONLY by passing events.
    /// </summary>
    readonly CustomerState _state;
    
    public Customer(IEnumerable<IEvent> events)
    {
       _state = new CustomerState(events);
    }
    

    Приведу код всего класса CustomerState, лишь уберу несколько методов проекции When.
    /// <summary>
    /// This is the state of the customer aggregate.
    /// It can be mutated only by passing events to it.
    /// </summary>
    public class CustomerState
    {
       public string Name { get; private set; }
       public bool Created { get; private set; }
       public CustomerId Id { get; private set; }
       public bool ConsumptionLocked { get; private set; }
       public bool ManualBilling { get; private set; }
       public Currency Currency { get; private set; }
       public CurrencyAmount Balance { get; private set; }
    
       public int MaxTransactionId { get; private set; }
    
       public CustomerState(IEnumerable<IEvent> events)
       {
           foreach (var e in events)
           {
               Mutate(e);
           }
       }
    ...
       public void When(CustomerCreated e)
       {
           Created = true;
           Name = e.Name;
           Id = e.Id;
           Currency = e.Currency;
           Balance = new CurrencyAmount(0, e.Currency);
       }
    
       public void When(CustomerRenamed e)
       {
           Name = e.Name;
       }
    
       public void Mutate(IEvent e)
       {
           // .NET magic to call one of the 'When' handlers with
           // matching signature
           ((dynamic) this).When((dynamic)e);
       }
    }
    

    Как видно в конструкторе мы форычем бежим по переданным ивентам, и передаем их в метод Mutate, который в свою очередь напрявляет их дальше в подходящий метод проекции.
    Можно заметить что все проперти имеют private setter метод, что гарантирует что состояние извне мы можем изменить только передав соответствующее событие.

    Состояние восстановили, теперь можно и попробовать его изменить. Вернемся немного назад к методу изменения состояние. Так как я начал разбираться с коммандой CreateCustomer, то и у агрегата посмотрим метод Create.
    public void Create(CustomerId id, string name, Currency currency, IPricingService service, DateTime utc)
    {
       if (_state.Created)
           throw new InvalidOperationException("Customer was already created");
       Apply(new CustomerCreated
           {
               Created = utc,
               Name = name,
               Id = id,
               Currency = currency
           });
    
       var bonus = service.GetWelcomeBonus(currency);
       if (bonus.Amount > 0)
           AddPayment("Welcome bonus", bonus, utc);
    }
    

    Здесь самое место сделать проверку наших бизнесс правил, так как у нас есть доступ к актуальному состоянию агрегата. У нас есть бизнесс правило что Customer может быть создан лишь один раз ( врочем еще есть и техническое ограничение), поэтому при попытки вызвать Create на уже созданном агрегате мы бросаем эксепшен.
    Если же все бизнесс правила удовлетворены то в метод Apply передаем событие CustomerCreated. Метод Apply очень прост, лишь передает событие объекту состояния и добавляет его в список текущих изменений.
    public readonly IList<IEvent> Changes = new List<IEvent>();
    readonly CustomerState _state;
    
    void Apply(IEvent e)
    {
       // pass each event to modify current in-memory state
       _state.Mutate(e);
       // append event to change list for further persistence
       Changes.Add(e);
    }
    

    В большенству случаев на одну операцию с аггрегатом приходится одно событие. Но вот как раз в случае с методом Create событий может быть два.
    После того как мы передали в метод Apply событие CustomerCreate, мы может добавить текущему кастомеру приветственный бонус, если удовлетворяетя бизнесс правило что сумма бонуса должена быть больше нуля. В таком случае вызывается метод агрегата AddPayment, который не сореджит никаких проверок а просто инициирует событие CustomerPaymentAdded.
    public void AddPayment(string name, CurrencyAmount amount, DateTime utc)
    {
       Apply(new CustomerPaymentAdded()
           {
               Id = _state.Id,
               Payment = amount,
               NewBalance = _state.Balance + amount,
               PaymentName = name,
               Transaction = _state.MaxTransactionId + 1,
               TimeUtc = utc
           });
    }
    

    Теперь предстоит сохранить новые события и опубликовать их в Read model. Подозреваю что это делает следующая строчка.
    // append resulting changes to the stream
    _eventStore.AppendToStream(id, stream.Version, customer.Changes);
    

    Убедимся…
    public void AppendToStream(IIdentity id, long originalVersion, ICollection<IEvent> events)
    {
       if (events.Count == 0)
           return;
       var name = IdentityToString(id);
       var data = SerializeEvent(events.ToArray());
       try
       {
           _appendOnlyStore.Append(name, data, originalVersion);
       }
       catch(AppendOnlyStoreConcurrencyException e)
       {
           // load server events
           var server = LoadEventStream(id, 0, int.MaxValue);
           // throw a real problem
           throw OptimisticConcurrencyException.Create(server.Version, e.ExpectedStreamVersion, id, server.Events);
       }
    
       // technically there should be a parallel process that queries new changes
       // from the event store and sends them via messages (avoiding 2PC problem).
       // however, for demo purposes, we'll just send them to the console from here
       Console.ForegroundColor = ConsoleColor.DarkGreen;
       foreach (var @event in events)
       {
           Console.WriteLine("  {0} r{1} Event: {2}", id,originalVersion, @event);
       }
       Console.ForegroundColor = ConsoleColor.DarkGray;
    }
    

    Ну почти угодал. События сериализуются и сохраняются в append only store (удалять и изменять их мы то не собираемся). Но вот вместо того чтобы отправить их в read-model (на презентационный уровень), Ринат просто выводит их на консоль.
    Впрочем для примера этого достаточно.
    Если вы хотите посмотреть как это все будет работать с очередью сообщений можете посмотреть пример на гитхабе из моей предыдущей статьи. Я его немного изменил и тоже внес часть инфраструктуры Event Sourcing’a в солюшен. На этом примере я хочу показать как можно добавить снэпшоты.

    Snapshots


    Снэпшоты нужны чтобы делать промежуточные снимки состояния аггрегата. Это позволяем не закгружать весь поток событий агрегата, а лишь только те которые произошли после того как мы сделали последний снэпшот.
    Итак посмотрим на реализацию.
    В классе UserCommandHandler есть метод Update очень похожий на тот что у Рината, но у меня он еще сохраняет и загружает снэпшоты. Снэпшоты делаем через каждые 100 версий.
    private const int SnapshotInterval = 100;
    

    Сначала поднимаем из репозитория снэпшот, потом загружаем поток событий которые произошли после того как мы сделали этот снэпшот. Затем передаем все это конструктору агрегата.
    private void Update(string userId, Action<UserAR> updateAction)
    {
       var snapshot = _snapshotRepository.Load(userId);
       var startVersion = snapshot == null ? 0 : snapshot.StreamVersion + 1;
       var stream = _eventStore.OpenStream(userId, startVersion, int.MaxValue);
       var user = new UserAR(snapshot, stream);
       updateAction(user);
       var originalVersion = stream.GetVersion();
       _eventStore.AppendToStream(userId, originalVersion, user.Changes);
       var newVersion = originalVersion + 1;
       if (newVersion % SnapshotInterval == 0)
       {
           _snapshotRepository.Save(new Snapshot(userId, newVersion,user.State));
       }
    }
    

    В конструкторе пытаемся достать состояние из снэпшота или создаем новое состояние если нету снэпшота. На полученном состоянии проигрываем все недостающие события, и в итоге получаем актуальную версию агрегата.
    public UserAR(Snapshot snapshot, TransitionStream stream)
    {
       _state = snapshot != null ? (UserState) snapshot.Payload : new UserState();
       foreach (var transition in stream.Read())
       {
           foreach (var @event in transition.Events)
           {
               _state.Mutate((IEvent) @event.Data);
           }
       }
    }
    

    После манипуляций с агрегатом, проверяем кратна ли версия агрегата интервалу через который мы делаем снэпшоты, и если это так, то сохраняем новый снэпшот в репозиторий. Чтобы получить из UserCommandHandler’a состояние агрегата пришлось сделать для него internal getter свойство State.

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

    Feedback


    Если вам интересна тема CQRS+ES пожалуйста не стесняйтесь задавать вопросы в комментариях. Так же можете писать мне в скайп если нету ака на хабре. Недавно мне в скайп постучался один товарищ из Челябинска и благодаря его вопросам у меня появилось много идей для следующей статьи. Так как в моем распоряжении сейчас побольше свободного времени то я планирую писать их почаще.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      0
      Вот мне не совсем понятно, как востанавливаются события из снапшотов.
      Скажем были у нас следующие события:
      create
      update name
      update password
      — snapshot — update password

      Если я всё правильно помимаю, то востанавливается объект из событий со времени последнего снапшота.
      Т.е. после снапшота объект получает только одно событие update password #2.

      Вопрос, что я упускаю в логике работы? Или как мне получить значение Name?
        0
        События в снэпшоте не храняться. В нем как раз такие храниться проекциях всех предыдущий событий — состояние агрегата. При обработки новой команды вам нужное получить состояние агрегата, чтобы проверить ваши бизнес правила. Если снэпшотов нету — вы получаете состояние путем проигровывания всех событий данного агрегата (поток событий). Если у вас есть снэпшот, можно сказать что у вас есть промежуточное состояние. Вы восстанавливаете это состояние
        _state = snapshot != null ? (UserState) snapshot.Payload : new UserState();
        

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

          Тогда следующий вопрос: А как поиск по этому всему бинарному зоопарку осуществлять? Сериализировать в XML и использовать XPath? А всякие там FullText search с не чётким поиском? А как мне найти пользователей имена которых начинаются на F и входящие группы имена которых начинаются на A или B?

          мне это всё нужно будет выгражуть из базы? Проигрывать все события? Или искать только по последним снапшотам, если таковые имеются. А если их нет?
            0
            Для всего этого есть read-model.
            После того как события сохранились в Event Store они отсылаются на презентационный уровень в Read-model. Концептуально это что-то вроде Views в SQL. Вы обрабатываете там события еще раз и обнавляете read-базу. А в качестве read-базы может быть и SQL и Lucenе и MongoDB, или вообще все вместе.
        0
        События сериализуются и сохраняются в append only store (удалять и изменять их мы то не собираемся).
        А вот это жесть. Я так понимаю, что если какое-то событие устаревает, то мы должны просто его больше не использовать и создавать новое? Через несколько лет, у нас будет CreateCustomerV200? А база данных это только Key-Value?

        Либо я чего-то не понимаю, либо подход весьма узкопрофильный и подходит только для систем построеных на событиях, которые очень и очень редко меняются.
          0
          О, придумала. Биржевые брокеры со всякими Форэксами будут счастливы от таких систем. Там одни и те же события уже много и много лет. Банки будут счастливы от такого подхода, там списал деньги — зачислил, упрощаю. Но, что-то я слабо верю в то, что это будет работать с привычными бизнес-сущностями, по которым нужно проводить аналитику, строить отчёты, я уже не говорю о всяких там ОLAP. И кажется это подход, который появился для KeyValue баз данных.
            0
            Вы можете добавлять в события поля, просто при десериализации инициализировать их значениями по умолчанию. Переименовывать поля и удалять нельзя (это можно обойти если добавить аттрибуты порядка). Да write-база выглядит как key-value если не смотреть под копот. Но это write-база, больше вам от неё ничего не нужно. Вы даже не сможете прочитать состояние агрегата извне, у него есть только методы изменения состояния. Для отображение есть read-база, там уже может быть что угодно. Можете посмотреть пример к моей предыдущей статье.
            0
            Про версии событий детальнее хочется услышать. Чтобы были примеры граблей, на которые можно наступить, если версии не использовать или использовать не верно.
            В Lokad, как я вижу, версия используется для снэпшота, чтобы определить к какому снэпшоту применять/восстанавливать события, так?
            Но ведь и сами события могут изменятся в процессе разработки, а стек событий уже может быть в базе и при большом восстановлении с нуля, возникает проблема конвертации.

            Хочется больше информации на этот счет с примерами. Так как теоретического материала много, но как-то это вскользь все рассматривается.
              0
              Без версий ничего не получится, так как важно знать в каком порядке воспроизводить события при восстановлении состояния агрегата. Event Stream — это поток событий для конкретного агрегата. Поток событий характеризуется тем что события с нем упорядочены. Например чтобы восстановить состояние агрегата User, нам нужен поток событий для этого юзера. У потока событий есть ID. Это тот же ID что и у Aggregate Root'a, в данном случае это ID пользователя. То есть получив поток событий с ID = 1 мы сможет по порядку воспроизвести все события которые происходили с пользователем с ID = 1. В итоге получится актуальное состояние пользователя. Если порядок поменять то мы уже не можем гарантировать что получим тоже самое состояние.
              В Lokad IDDD Sample снэпшотов нету.
                0
                На счет проблем с конвертацие, можете уточнить что именно вы имеете в виду, а то их тут может быть много и в разных местах.
                  0
                  Например в событии было сначала только Name, а потом разделилось на LastName, FirstName. Хотя событие остается одно и то же, пусть будет RenameSomething().
                  Это ведь real-world case
                    0
                    Самый простой вариант добавить LastName и FirstName, а в коде уже обрабатывать ситуацию когда они не заданы. Но тут тема, конечно, для отдельной статьи. Раньше мы писали патчи, которые проходились по всему event store и меняли устаревшие события на новый формат. Однако, я сейчас склоняюсь к тому, что это не правильных подход.
                +1
                Возможно будет интересно почитать. MS Patterns & Practices совсем недавно выпустило CQRS Journey — The project is focused on building highly scalable, highly available and maintainable applications with the Command & Query Responsibility Segregation and the Event Sourcing patterns.
                  0
                  Спасибо. Даже не знал что они уже закончили, читал на гитхабе черновики.
                  0
                  Почитал пример на github, сразу же возник вопрос (который собственно в статье и не освещён): у класса UserAR есть только методы по изменению его состояния. Что логично, исходя из всей идеи — объект же будет получать только команды по обновлению своего состояния, а отображаться пользователю данное состояние будет только из read-базы. Но что делать, когда в предметной области необходимо провернуть взаимодействие между двумя объектами? Очевидно, что при таком раскладе чьё-то состояние для начала придётся прочитать. Где его взять то?

                  Конкретно по 2й части статьи: мне одному кажется, что UserAR, UserState, UserИзReadБазы — это уже немножко много и очень похоже на разработку вокруг конкретной технологии, а никак не вокруг модели (DDD)?
                    0
                    Видите ли, проблема тут в способе хранения данных, как я поняла. То, о чём вы говорите это привычные связи one-to-one, one-to-many, many-to-many. Подобные связи сложно строятся в базах данных key-value based, и там для построения этих связей нужно прилагать не человеческие усилия, я уже не говорю о каскадных обновлениях и удалениях объектов.

                    Первое, что приходит в голову это реализовывать CompositeCommand которая будет в себе агрегировать команды по обновлению состояний объектов. Но, если сейчас маршрутизация этого всего т.с. direct routing, то для реализации обновлений связей прийдётся реализовывать уже broadcast'ы и всякие там unicast'ы, что в сотни раз усложняет задачу. Для примера, вспомните свои первые приложения на WinAPI где приходилось следить, кто и кому что отправил. Доводилось мне работать в бытности и с Flex'ом, где внутри использовался PureMVC — система страдала как раз от широковещательных отправок.

                    Более того, даже если и можно было бы пойти этим путём возникает вопрос транзакционности этого всего дела.
                    Реально, я слабо себе представляю как это всё можно за приемлемое время и деньги разрулить, т.к. в качестве хранения используется MongoDB, то и всякие MSDTC тоже тут не сильно помогут.

                    Проблем у паттерна очень много, и как я уже говорила это всё подходит только для биржевых систем и других событийно ориентрованых систем, но уж ни как не для CRM'ов, ERP и прочих монстров.
                      0
                      У меня в голове всё же был более простой пример. Ну допустим, корзина в интернет-магазине. Она состоит из позиций (наименование товара, кол-во, сумма). Пользователю нужно показать «Итого». Или сгенерировать правильную сумму для оплаты через платёжную систему. Т.е. для всех добавленных в корзину товаров — получить стоимость каждого, с учётом желаемого количества. Глядя на исходники на github, не представляю как реализовать подобное поведение. Прямо вот напрашивается disclaimer к статье, гласящий о том, что данный подход актуален лишь для некоторого подмножества систем.
                        0
                        Для этого есть специальный термин: Перепроектирование. Если уж так хочется:

                        public class Order
                        {
                        public ICollection Items { get; set;}
                        public decimal Total
                        {
                        get { Items.Sum(i=>i.Price * i.Quantity); }
                        }
                        }

                        Это не совсем по фэншую, но если не стоит задача написания фабрик по выпуску абстрактных фабрик калькуляторов, то это самое оно.
                          0
                          Так ведь, согласно описанию, доступа на чтение к модели у нас нет!
                            0
                            В рамках одного агрегата у вас есть доступ ко всему. Достаточно сделать так чтобы нужные вам сущности оказались в этих рамках.
                            0
                            Так как у вас корее всего будет событие OrderItemAdded, вам достаточно будет написать так
                            public class OrderState
                            {
                                public ICollection Items { get; private set; }
                                public decimal Total { get; private set; }
                            
                                public void On(OrderItemAdded e)
                                {
                                    var item = new Item(e.Price, e.Quantity);
                                    Items.Add(item);
                                    Total += e.Price + e.Quantity;
                                }
                            }
                            

                            В read моделе вы тоже можете обработать это событие подобным образом.
                            0
                            А если говорить о заказах и товарном учёте, то используя этот подход с событиями, вы очень много времени потратите на реализацию редактирования приходных и расходных ордеров задним числом. Нет, я всё понимаю, я взрослая девочка, но таковы реалии — очень большое кол-во ПО хотят повторять гибкость Excel'я. Я в бытности знала один банк, который 5 лет вёл ВСЮ калькуляцию в Excel'e, с макросами и прочими плюшками, но факт остаётся фактом.
                              0
                              Если вам надо просто показать «Итого», то тут нету никаких проблем. Вы можете хранить в read модели данные как вам захочется. Можете хранить где-то список позиций для пользователя и каждый раз считать итоговую сумму, или вообще хранить сразу готовую итоговую сумму и обновлять её когда приходит события добавления новой позиции.
                              Проблемы начнутся когда у вас появится бизнес правило которое зависит от итоговой суммы. Но это задача тоже решается, просто надо правильно спроектировать агрегат, чтобы при добавлении юзером новой позиции у агрегата был доступ к итоговой сумме.
                                0
                                Можете хранить где-то список позиций для пользователя и каждый раз считать итоговую сумму
                                Вот меня и интересует, где хранить список позиций.
                                Да, «итого» может быть в read-модели. Но инкрементировать его нельзя, мы же не можем использовать данное значение, находясь в контексте write-модели.
                            +1
                            А от вот этого:
                            public void Mutate(IEvent e)
                            {
                            // .NET magic to call one of the 'When' handlers with
                            // matching signature
                            ((dynamic) this).When((dynamic)e);
                            }

                            Мне плакать хочется. Только мы получили строго типизированый язык, как сразу же пошли в обратном направлении. Динамики в первую очередь удобны при работе со всякими там COMами, ActiveXами и прочими IUnknown
                              0
                              Ну это чисто для удобства, чтобы не использовать рефлексию.
                              Можно обойтись и без динамиков и рефлексии, просто придется явно писать интерфейс для обработки каждого события.
                              0
                              Ну давайте сначала определимся с терминологией. Агрегат — это не одна сущность, это несколько связанных сущностей, к которым мы можем обращаться только через Aggregare Root. Aggregare Root — это корневая сущность агрегата. Когда вы проектируете систему надо как-раз таки выделять агрегаты так, чтобы вам потом было удобно с ними работать в рамках вашей предметной области. Если у вас при изменение какой-то сущности важно знать состояние юзера, то поместите эту сущность в User агрегат. Выглядеть это будет как вложенный объект или коллекция объектов в объекте состояния агрегата. Для того чтобы изменить эту сущность вы будете обязаны обращаться через агрегат, а не на прямую. Нету ни какого смысла выделять по агрегату для каждой сущности из вашей предметной области, это лишь её усложнит. Как раз суть проектирования агрегатов и заключается в том чтобы грамотно скомпоновать сущности.
                                0
                                я подумаю над этим вечером, завтра скажу.
                                Тут тоже не всё так просто, с подобными графами.
                                  0
                                  Если у вас при изменение какой-то сущности важно знать состояние юзера, то поместите эту сущность в User агрегат.

                                  В пределе тогда вся логика окажется в классе UserAR :)
                                  Своим вопросом я пытаюсь понять, как предлагается реализовывать непосредственно правила бизнес-логики. Всегда будет необходимость во взаимодействии двух или более объектов. И они легко могут быть из разных агрегатов. Но подход с сокрытием состояния явно мешает подобному взаимодействию. Может просто исходная методология этого не подразумевает на самом деле?
                                    0
                                    Если у вас все-таки не получилось спроектировать систему так, что любое бизнес правило может быть проверено в рамках одного агрегата, то это проблема, но проблема решаемая.
                                    Я могу вам предложить 3 способа.
                                    1. Самый простой способ — это проверять бизнес правило по данным read модели перед отправкой команды. Но так как в этом случае данные read модели могут быть не консистентны, способ стоит применять только если ваша бизнес модель допускает вероятность ошибки при проверке этого правила, так как на время отправки команды в read модели могут находится не актуальные данные. Вероятность конечно очень мала но она есть. Мы часто используем этот подход так как он самый простой и не накладывает дополнительных ограничений.
                                    2. Вы можете в Command Handler'e восстановить два агрегата, сначала попытаться изменить состояние одного, в котором проверяется бизнес правило. Если не возникло исключительной ситуации, изменить состояние второго агрегата. Но это накладывает ограничение на масштабируемость так как теперь вы не можете разделить эти два агрегата.
                                    3. Самый концептуально правильный подход — это организовать месседжинг между агрегатами. Т.е. вы сначала отправляете команду в один агрегат, там проверяется ваше бизнес правило, инициируется событие, в обработчике этого события в отправляете команду в другой агрегат, которая уже содержит информацию о том что бизнес правило выполнилось.
                                      0
                                      Что значит если не получилось? Это вполне нормальная ситуация, когда во взаимодействии участвует несколько агрегатов. И я не говорю про бизнес-правила (их проверку организовать относительно несложно). Разговор идёт про получение данных из write-модели в целом и от разных агрегатов в частности.
                                        0
                                        Ну не получилось например потому, что вы не учли некоторые детали при проектировани и они добавились позже, когда уже нету возможности заново проектировать домен. Это конечно плохо, но всякое случается. В случае с CQRS концептуально неправильно получать данные из write модели. Единственное что вам может вернуть агрегат — это исключение. Конечно вы можете это правило игнорировать, но тогда могут возникнуть проблемы с масштабируемостью. Так же команда должна относить только к одному агрегату, поэтому поднимать какой-либо другой тоже концептуально не правильно, не говоря уже о том чтобы открыть доступ извне к его состоянию.
                                          0
                                          Это вполне нормальная ситуация с точки зрения DDD. Но CQRS накладывает дополнительные ограничения в угоду масштабируемости.
                                            +1
                                            Вот было бы просто отлично, если бы вы в 1й статье и написали границы применимости и основные ограничения данного подхода. А то те, кто не в курсе — побегут усложнять их корпоративные хелловорлды! :-D
                                              0
                                              Границ применимости я не вижу, честно говоря, и не думаю что они есть. А ограничений у данного подхода не больше чем у ORM, при том что возможности намного шире.
                                              Постараюсь в следующих статьях это продемонстрировать. Спасибо за ваши комментарии.
                                                0
                                                Всё же Фаулер и Udi приходят к одинаковому мнению по поводу наличию этих границ. Вчитайтесь!
                                                  0
                                                  Они на счет границ ничего не говорят. Говорят только, что в некоторых случаях лучше избегать применения CQRS, чтобы не усложнить систему. У Udi даже пост есть «When to avoid CQRS»
                                  0
                                  Пять лет прошло, где же все эти кучи статей…

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

                                  Самое читаемое