Агрегат как услуга

Предположим сценарий, в котором службе требуется некоторая глобальная конфигурация для обработки некоторого запроса.

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

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

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

ConfigService {
@Inject
configRepository;
@Inject
eventGateway;

@CommandHandler
handle(changeConfig){
let current = configRepository.loadCurrent;
//some checks

//persist here?
eventGateway.send(confgChanged)
}

@EventHandler
on(configChanged){
//or persist here?
configRepository.saveCurrent(configChanged.data)
}

}

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

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


person PolishCivil    schedule 02.05.2019    source источник


Ответы (2)


Мне кажется, что ваша глобальная конфигурация является либо спецификацией или набор правил, как в системе правил.

В отличие от шаблонов, описанных в книге GOF, в DDD , некоторые строительные блоки / шаблоны являются более общими и могут применяться к различным типам объектов, которые у вас есть.

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

Объект значения - это то, что не имеет идентичности, (большую часть времени) является неизменным, два экземпляра можно сравнить по равенству их свойств. Объект значения представляет важные концепции в наших областях, например: Деньги в системе, которая занимается бухгалтерским учетом, банковским делом и т. д., Vector3 и Matrix3 в системах, выполняющих математические вычисления и моделирование, таких как системы моделирования (3dsMax, Maya), видеоигры и т. д. Они содержат важные особенности.

Таким образом, все, что вам нужно отслеживать и что имеет идентификационные данные, может быть Entity.

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

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

Спецификации и Правила также могут быть Доменными службами.

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

Вот пример.

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

Допустим, у нас есть правило, которое гласит: если Клиент сделал Заказ с более чем 5 <сильными > LineItems он получает скидку. Если этот Заказ имеет общую стоимость некоторой суммы (скажем, 1000 долларов США), он получает скидку.

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

Система продаж и система заказов могут быть частью двух отдельных ограниченных контекстов < / em>: Продажи и Заказы. Ограниченный контекст заказов зависит от ограниченного контекста продаж.

Примечание: я пропущу большинство деталей реализации и добавлю только соответствующие вещи, чтобы сократить и упростить этот пример. Если намерение неясно, я отредактирую и добавлю дополнительные сведения. UUID, DiscountPercentage и Деньги - это объекты-значения, которые я пропущу.

public interface OrderDiscountPolicy {

    public UUID getID();

    public DiscountPercentage getDiscountPercentage();
    public void changeDiscountPercentage(DiscountPercentage percentage);

    public bool canApplyDiscount(Order order);
}

public class LineItemsCountOrderDiscountPolicy implements OrderDiscountPolicy {

    public int getLineItemsCount() { }

    public void changeLineItemsCount(int count) { }

    public bool canApplyDiscount(Order order) { 
        return order.getLineItemsCount() > this.getLineItemsCount();
    }

    // other stuff from interface implementation
}

public class PriceThresholdOrderDiscountPolicy implements OrderDiscountPolicy {

    public Money getPriceThreshold() { }

    public void changePriceThreshold(Money threshold) { }

    public bool canApplyDiscount(Order order) { 
        return order.getTotalPriceWithoutDiscount() > this.getPriceThreshold();
    }

    // other stuff from interface implementation
}

public class LineItem {

    public UUID getOrderID() { }
    public UUID getProductID() { }
    public Quantity getQuantity { }
    public Money getProductPrice() { } 

    public Money getTotalPrice() {
        return getProductPrice().multiply(getQuantity());
    }
}

public enum OrderStatus { Pending, Placed, Approced, Rejected, Shipped, Finalized }

public class Order {

    private UUID mID;
    private OrderStatus mStatus;
    private List<LineItem> mLineItems;
    private DscountPercentage mDiscountPercentage;

    public UUID getID() { }
    public OrderStatus getStatus() { }
    public DscountPercentage getDiscountPercentage() { };

    public Money getTotalPriceWithoutDiscount() { 
        // return sum of all line items
    }

    public Money getTotalPrice() { 
        // return sum of all line items + discount percentage
    }

    public void changeStatus(OrderStatus newStatus) { }

    public List<LineItem> getLineItems() {
        return Collections.unmodifiableList(mLineItems);
    }

    public LineItem addLineItem(UUID productID, Quantity quantity, Money price) {
        LineItem item = new LineItem(this.getID(), productID, quantity, price);
        mLineItems.add(item);
        return item;
    }

    public void applyDiscount(DiscountPercentage discountPercentage) {
        mDiscountPercentage = discountPercentage;
    }
}

public class PlaceOrderCommandHandler {

    public void handle(PlaceOrderCommand cmd) {

        Order order = mOrderRepository.getByID(cmd.getOrderID());

        List<OrderDiscountPolicy> discountPolicies =
             mOrderDiscountPolicyRepository.getAll();

        for (OrderDiscountPolicy policy : discountPolicies) { 

            if (policy.canApplyDiscount(order)) {
                order.applyDiscount(policy.getDiscountPercentage());
            }
        }

        order.changeStatus(OrderStatus.Placed);

        mOrderRepository.save(order);
    }
}

public class ChangeOrderDiscountPolicyPercentageHandler {

    public void handle(ChangeOrderDiscountPolicyPercentage cmd) {

        OrderDiscountPolicy policy = 
            mOrderDiscountRepository.getByID(cmd.getPolicyID());

        policy.changePercentage(cmd.getDiscountPercentage());

        mOrderDiscountRepository.save(policy);
    }
}

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

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

Допустим, у нас есть 2 службы: OrdersService и OrdersDiscountService.

Есть несколько способов реализовать эту операцию. Мы можем использовать:

  • Хореография с событиями
  • Оркестровка с явным использованием Saga или диспетчера процессов

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

  1. CreateOrderCommand -> OrdersService -> OrderCreatedEvent

  2. OrderCreatedEvent -> OrdersDiscountService -> OrderDiscountAvailableEvent или OrderDiscountNotAvailableEvent

  3. OrderDiscountAvailableEvent или OrderDiscountNotAvailableEvent -> OrdersService -> OrderPlacedEvent

В этом примере для размещения заказа OrdersService будет ждать OrderDiscountNotAvailableEvent или OrderDiscountNotAvailableEvent , чтобы он мог применить скидку перед изменением статуса заказа на OrderPlaced.

Мы также можем использовать явную Saga для оркестрации между сервисами.

Эта сага будет содержать последовательность шагов процесса, чтобы он мог его выполнить.

  1. PlaceOrderCommand -> Сага
  2. Saga запрашивает у OrdersDiscountService, доступна ли скидка на этот заказ .
  3. Если доступна скидка, Saga вызывает OrdersService, чтобы применить скидку.
  4. Saga вызывает OrdersService, чтобы установить статус Order на OrderPlaced

Примечание. Шаги 3 и 4 можно комбинировать

Возникает вопрос: * "Как OrdersDiscountService получает всю необходимую информацию для Заказа для расчета скидок?" *

Это может быть достигнуто путем добавления всей информации о заказе в Событие, которое будет получать эта служба, или с помощью OrdersDiscountService позвоните в OrdersService, чтобы получить информацию.

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

Преимущество оркестрации с сагой заключается в том, что точный процесс явно определен в саге < / strong> и его можно найти, понять и отладить.

Наличие неявных процессов, таких как Хореография с событиями, может быть труднее для понимания, отладки и поддержки.

Обратной стороной саг является то, что мы определяем больше вещей.

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

Вот несколько дополнительных ресурсов:

https://blog.couchbase.com/saga-pattern-implement-business-transactions-using-microservices-part/

https://blog.couchbase.com/saga-pattern-implement-business-transactions-using-microservices-part-2/

https://microservices.io/patterns/data/saga.html

Архитектура LMAX очень интересна для чтения. Это не распределенная система, но управляемая событиями и записывает как входящие события / команды, так и исходящие события. Это интересный способ фиксировать все, что происходит в системе или службе.

person expandable    schedule 05.05.2019
comment
Спасибо за еще один обширный ответ, очень помогает! Небольшой уточняющий вопрос, не могли бы вы проверить, правильно ли я думаю. Рассматривая сценарий микросервиса в представленном вами примере заказов и скидок, поскольку я не буду смешивать репозитории в кодовых базах, PlaceOrderCommand в основном запустит сагу и спросит у службы скидок, какие скидки доступны для заказа. Значит, тогда сервису скидок просто нужно будет прислушиваться к событиям создания заказов и создавать свой собственный «имидж» заказов, которые будут спрашивать его о доступных для них скидках? - person PolishCivil; 05.05.2019
comment
Пожалуйста. рад помочь :) Извините .. Надо было добавить и распределенный кейс. Я добавлю правку к своему ответу. Ваше мнение правильное :) Только кое-что о «маге» в DiscountService. Если у вас нет записи об обработанных заказах в этой службе, вы можете записывать такие изображения. С другой стороны, этот сервис может рассчитывать скидки, когда кто-то их спрашивает (например, Saga), и возвращать их, не сохраняя никакой информации, тем самым экономя место для хранения. Как вы это сделаете, будет зависеть от того, насколько закончен случай. В этом примере я думаю, что в этом нет необходимости. - person expandable; 05.05.2019

Вы здесь используете Axon без источника событий?

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

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

Конкретный пример: представьте, что ваш агрегат обработал команду на включение почтовой службы. Агрегат сделал это, применив событие EmailServiceEnabledEvent и изменив свое состояние на boolean emailEnabled = true. Через некоторое время агрегат выгружается из памяти. Теперь вы измените этот конфигурационный репозиторий, чтобы отключить включение почтовой службы. Когда агрегат снова загружается, применяются события из хранилища событий, но на этот раз он загружает конфигурацию из вашего репозитория, в которой говорится, что он не должен включать службу электронной почты. Состояние boolean emailEnabled оставлено false. Вы отправляете команду отключения службы электронной почты в совокупность, но обработчик команд в совокупности считает, что электронная почта уже отключена, и не применяет событие EmailServiceDisabledEvent. Электронная почта остается включенной.

Вкратце: я бы рекомендовал использовать команды для изменения конфигурации вашего агрегата.

person Mzzl    schedule 06.05.2019