Aggregate Що робити, коли нам все таки потрібні різні частинк | Beer::PHP 🍺

Aggregate

Що робити, коли нам все таки потрібні різні частинки, бо вони приймають участь в одній операції? Наприкад, створення Invoice для того самого Order (продовжуємо думку попереднього поста) може вимагати як особистих даних користувача, так і повного списку товарів (щоб їх там відобразити), а також загальної суми для транзакції. Тут на поміч приходить агрегат.

Агрегат — група пов’язаних об’єктів домену. Він складається з однієї або декількох сутностей (а інколи й об’єктів-значень), які логічно пов’язані між собою.

У нашому випадку, коли для створення інвойсу потрібні дані користувача, список товарів і сума, агрегат має зібрати все це разом і гарантувати, що зміни відбуватимуться транзакційно. Наприклад, якщо додати новий товар, то загальна сума автоматично оновиться.

Агрегат має корінь (aggregate root), через який відбуваються всі операції. Він відповідає за забезпечення інваріантів (бізнес-правил), надання методів для доступу та зміни стану, щоб зовнішні виклики не могли порушити ту саму узгодженість. Це означає, що замість того, щоб окремо взаємодіяти з обʼєктами даних користувача, товарами й сумою, ви працюєте з одним об’єктом, який уже все це тримає в узгодженому стані.

[golang] [php] [python] [nodejs]
// Клас для даних користувача
final class UserData {
public function __construct(
public string $name,
public Address $address
) {}
}

/**
* @param Product[] $items
*/
final class Invoice {
public function __construct(
public OrderId $orderId,
public UserData $user,
public array $items,
public Money $total
) {}
}

// Агрегат для замовлення
final class OrderAggregate {
private OrderId $orderId;
private UserData $user;
private array $items;
private Money $total;

public function __construct(OrderId $orderId, UserData $user, array $items) {
$this->orderId = $orderId;
$this->user = $user;
$this->items = $items;
$this->calculateTotal();
}

// Розрахунок загальної суми
private function calculateTotal(): void {
$this->total = array_sum(array_map(fn($item) => $item->price * $item->amount, $this->items));
}

// Додавання нового товару
public function addItem(string $name, Money $price, int $quantity): void {
$this->items[] = new Product($name, $price, $quantity);
$this->calculateTotal();
}

// Створення інвойсу
public function createInvoice(): Invoice {
return new Invoice($this->orderId, $this->user, $this->items, $this->total);
}
}

// Використання
$user = new UserData('Іван Петренко', new Address('Київ', 'вул. Шевченка', '1'));

$order = new OrderAggregate('123', $user, [new Product('Ноутбук', 20000, 1)]);
$order->addItem('Мишка', 500, 2);
$invoice = $order->createInvoice();
echo \sprintf(‘Інвойс для замовлення %s, користувач: %s, сума: %s грн\n’, $invoice->orderId, $invoice->user->name, $invoice->total);


Коли вам потрібно створити інвойс, ви викликаєте createInvoice(), і агрегат повертає об’єкт Invoice із усіма потрібними даними. Вам не доводиться вручну збирати ці частинки з різних джерел чи турбуватися про їхню узгодженість — агрегат це робить за вас.

Підсумуємо:
Всі зміни в агрегаті виконуються узгоджено. Після завершення транзакції стан усіх сутностей агрегата має бути консистентним.
Агрегати визначають область транзакції – операції над ними мають виконуватися атомарно. Наприклад, якщо ми щось зберігаємо, або дістаємо зі storage, то ми маємо це робити з усім агрегатом. Дані не можуть бути оновлені частково.
Зовнішні об’єкти можуть взаємодіяти лише з агрегатним коренем, що дозволяє зберігати внутрішню цілісність агрегата.

Агрегат не повинен містити інших агрегатів. Кожен агрегат має свою власну область відповідальності. Якщо вам потрібно з якоїсь причини повʼязати агрегати — використовуйте ідентифікатори для того щоб зробити посилання на інший агрегат.

#backend #architecture #middle #source
Beer::PHP 🍺

Beer::PHP 🍺

@beerphp
2.01K Подписчиков
Технологии Категория
Тут публікуються короткі замітки про PHP, Linux, Unit Testing, DB, OOP тощо, витяги зі статей, книг, відео, курсів та інших ма...