Как сериализовать / десериализовать события в хранилище событий и так ли плохо хранить в них агрегированные корни?

Я пытаюсь сделать приложение CQRS источником событий в PHP. И мне интересно, нормально ли ставить агрегатный рут (AbstractItem в следующем примере) в событие, которое сериализовано в БД? (Я полагаю, нет, но что является альтернативой?) Например, у меня есть обработчик команды для AddItemToCartCommand с помощью этого метода дескриптора:

    public function handle(Command $command)
{
Assertion::isInstanceOf($command, AddItemToCartCommand::class);

$cart = $this->loadCart($command->getCartId());
$item = $this->loadItem($command->getItemId());

$cart->addItem($item);

$this->eventRepository->save($cart);
}

В то время как Cart а также AbstractItem являются совокупными корнями.
мой Cart AR реализован так:

class Cart extends AggregateRoot
{

/** @var UuidInterface */
private $customerId;

/** @var AbstractItem[] */
private $items;

public function __construct(UuidInterface $cartId, UuidInterface $customerId)
{
$this->apply(new EmptyCartCreated($cartId, $customerId));
}

public function addItem(AbstractCartItem $item)
{
$this->apply(new ItemToCartAdded($this->getId(), $item));
}

protected function applyEmptyCartCreated(EmptyCartCreated $event)
{
$this->setId($event->getCartId());
$this->customerId = $event->getCustomerId();
}

protected function applyItemToCartAdded(ItemToCartAdded $event)
{
$item                                 = $event->getItem();
$this->items[(string) $item->getId()] = $item;
}
}

Теперь проблема с ItemToCartAdded Событие, которое имеет такую ​​структуру:

class ItemToCartAdded extends AbstractDomainEvent
{

/** @var UuidInterface */
protected $cartId;

/** @var AbstractItem */
protected $item;

public function __construct(UuidInterface $cartId, AbstractItem $item)
{
$this->cartId = $cartId;
$this->item   = $item;
}

public function getCartId(): UuidInterface
{
return $this->cartId;
}

public function getItem(): AbstractItem
{
return $this->item;
}
}

Может быть, я должен иметь некоторый объект DTO для хранения AbstractItem данные вместо AbstractItem Сама АР в ItemToCartAdded событие. Тогда я бы в applyItemToCartAdded метод просто создать новый AbstractItem объект на основе этих данных DTO.

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

С другой стороны, сериализация целого агрегатного корня приводит меня к проблеме с сериализацией: когда я сериализую AR, мне нужно в какой-то момент десериализоваться, но как мне это сделать? Я знаю, что есть рефлексия, но это отвратительный хак, который обходит мою проверку AR при десериализации, и, таким образом, я могу в итоге получить как-то недопустимый AR, который может превратиться в кошмар. Или нет?

Как мне сериализовать и десериализовать мои события, чтобы хорошо решить эту проблему? Может быть, я мог бы иметь в ItemToCartAdded события только идентификаторы этих двух агрегатов, но тогда я не смог бы применить это событие к корзине AR, чтобы оно имело список AbstractItems (чтобы в конечном итоге защитить некоторые из моих инвариантов). Я бы закончил с просто списком идентификаторов элементов в корзине AR, так как, конечно, у меня нет доступа к хранилищу в моем AR.

Где моя проблема здесь или в чем я ошибаюсь?

Кстати, я использую для этой развилки этой библиотеки: https://github.com/beberlei/litecqrs-php

0

Решение

Я пытаюсь сделать приложение CQRS источником событий в PHP. И мне интересно, нормально ли помещать агрегатный корень (AbstractItem в следующем примере) в событие, которое сериализуется в БД? (Полагаю, нет, но какая альтернатива?)

Нет, ты не можешь. Я согласен с @Codescribler.
Я думаю, что у вас есть проблемы с вашим дизайном.

Тогда как Cart и AbstractItem являются совокупными корнями.

Почему ты сделал AbstractItem Aggregate root? Что делает инвариант AbstractItem защищает? Какие Commands это обрабатывает и что Events доходность? Это ваша главная проблема дизайна. И вы должны назвать его в соответствии с вашим Ubiquitous language, лайк Product или если вам нужно более абстрактное имя, то CartItem,

Product Агрегированный корень, но в другом ограниченном контексте, например Inventory, Или это может быть объект CRUD, если не нужно защищать какие-либо инварианты. В этом BC он занимается созданием, изменением и удалением товара из вашего магазина.

Вы сейчас в Ordering bounded context и вы должны пересмотреть правила домена в этом BC. Итак, каковы ваши правила? Вы разрешаете изменение цены в Inventory До н.э., чтобы распространяться на элементы в Cart? Я так не думаю. Ваши клиенты будут очень злы, по крайней мере, без уведомлений об изменении цен с момента добавления товара в корзину. Итак, предположим, что как только товар был добавлен в корзину, цена зависает. Так что это будет неизменным.

Теперь о Cart, Какая информация вам нужна о Products которые добавляются к Cart чтобы защитить ваш инварианты? Каковы Cart инварианты? Ну, вы могли бы иметь максимум Order стоимость, скажем, 10000 грн. Так что вам нужно идентифицировать Product и Product price,
Итак, CartItem является неизменным Value Object, содержащий ProductId а также ProductPrice,
Итак, ваш код должен выглядеть так:

namespace Domain\Ordering;

class Cart
{
/** @var CartItem[] */
private $items = [];

const MAXIMUM_CART_VALUE = 10000;

public function handleAddItemToCart(AddItemToCart $command)
{
if ($this->getTotalCartValue() + $command->getItem()->getTotalItemPrice() > self::MAXIMUM_CART_VALUE) {
throw new \Exception(sprintf("A cart value cannot be more than %d USD", self::MAXIMUM_CART_VALUE));
}

yield new AnItemWasAddedToCart($command->getCartId(), $command->getItem());
}

public function applyAnItemWasAddedToCart(AnItemWasAddedToCart $event)
{
$this->items[] = $event->getItem();
}

private function getTotalCartValue()
{
return array_reduce($this->items, function (float $acc, CartItem $item) {
return $acc + $item->getTotalItemPrice();
}, 0.0);
}
}

class AddItemToCart implements Command
{

/**
* @var CartId
*/
private $cartId;
/**
* @var CartItem
*/
private $item;

public function __construct(
CartId $cartId,
CartItem $item
)
{
$this->cartId = $cartId;
$this->item = $item;
}

public function getCartId(): CartId
{
return $this->cartId;
}

public function getItem(): CartItem
{
return $this->item;
}
}

class AnItemWasAddedToCart implements Event
{
/**
* @var CartId
*/
private $cartId;
/**
* @var CartItem
*/
private $item;

public function __construct(
CartId $cartId,
CartItem $item
)
{
$this->cartId = $cartId;
$this->item = $item;
}

public function getCartId(): CartId
{
return $this->cartId;
}

public function getItem(): CartItem
{
return $this->item;
}
}

class CartId
{
//...
}

class ProductId
{
//...
}

class CartItem
{
/**
* @var ProductId
*/
private $productId;
/**
* @var float
*/
private $pricePerUnit;
/**
* @var int
*/
private $quantity;

public function __construct(
ProductId $productId,
float $pricePerUnit,
int $quantity
)
{
$this->productId = $productId;
$this->pricePerUnit = $pricePerUnit;
$this->quantity = $quantity;
}

public function getProductId(): ProductId
{
return $this->productId;
}

public function getTotalItemPrice()
{
return $this->pricePerUnit * $this->quantity;
}
}
1

Другие решения

Простой ответ — вы не помещаете агрегат в событие.

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

Почему плохое хранение AR в событии?

Событие является неизменным. AR по определению изменчив. AR также инкапсулируется, что делает сериализацию в лучшем случае неудобной.

1

По вопросам рекламы [email protected]