<? phpukraine PHP-ПЛАТФОРМА
⌕Пошук по платформі
КОНЦЕПЦІЯ DDD · КРОК 4 З 10

Сутність і агрегатAggregate

Група обʼєктів, які змінюються разом і разом відповідають за одне правило. Ззовні до неї звертаються лише через один обʼєкт, корінь.

4 хв читання Middle → Senior
01 · ЩО ЦЕ

Обʼєкти, які не живуть окремо

Уявіть відгук кандидата на вакансію. У нього є етап (новий, співбесіда, офер), історія змін і підсумок. Ці частини не живуть окремо: після підсумку етап більше не змінюється, а кожна зміна етапу лишає запис в історії. Це і є : правило, яке має бути правдою після кожної зміни.

СЛОВНИКІнваріант. Правило, яке має бути правдою після будь-якої зміни. Наприклад: у відгуку з підсумком етап більше не змінюється.

Агрегат обводить такі обʼєкти однією межею. Усередині неї є : єдиний обʼєкт, через який іде будь-яка зміна. Етап, запис історії й підсумок видно зсередини, але не ззовні.

СЛОВНИККорінь агрегату. Єдиний обʼєкт агрегату

Межа агрегату збігається з : агрегат зберігають цілком або не зберігають зовсім. Якщо одна дія змінює два агрегати, решту системи повідомляє , а не спільна транзакція.

СЛОВНИКМежа транзакції. Усе, що зберігається атомарно: або цілком, або ніяк. Для агрегату це він сам.
СЛОВНИКДоменна подія. Запис про те, що сталося в домені, у минулому часі: ApplicationWithdrawn. Інші частини системи реагують на неї окремо.
02 · МЕЖА

Вхід лише через корінь

АГРЕГАТ «ВІДГУК»межа транзакціїwithdraw()корінь агрегатуApplicationmoveTo() · withdraw():Stage:HistoryEntry:OutcomeControllerReportJob
АГРЕГАТ «ВІДГУК»межа транзакціїwithdraw()корінь агрегатуApplicationmoveTo() · withdraw():Stage:HistoryEntry:OutcomeControllerReportJob
Controller змінює відгук лише через корінь Application. ReportJob, який лізе в Stage напряму, порушує межу.
Ключовий учасник Домен Клієнт ✕ заборонено
  • Агрегат «Відгук» (межа транзакції) містить: Application, Stage, HistoryEntry, Outcome.
  • Controller використовує Application (withdraw()).
  • Application використовує Stage.
  • Application використовує HistoryEntry.
  • Application використовує Outcome.
  • ReportJob не може змінювати Stage напряму, лише через корінь.
  1. 1Ззовні тримають посилання лише на корінь. Інші агрегати знають його за ID, а не за обʼєктом.
  2. 2Будь-яка зміна йде методом кореня з доменною назвою: withdraw(), а не setStatus().
  3. 3Одна транзакція змінює один агрегат. Решта дізнається про зміну з доменної події.
03 · ЯК У PHP UKRAINE

Агрегат «Відгук» у нашому коді

Тут агрегат «Відгук»: змінює етап, записує подію в історію й дає кандидату відкликати відгук лише доти, доки в нього немає підсумку.

php Application.php фрагмент, скорочено
1final class Application
2{
3 public function moveTo(ApplicationStatus $status, DateTimeImmutable $now, string $actor = ''): bool
4 {
5 // A refusal can be taken back by moving the card; an outcome cannot.
6 if ($this->outcome !== null) {← інваріант
7 throw new InvalidArgumentException('Розгляд цього відгуку завершено: '.$this->outcome->label().'.');
8 }
9
10 if ($this->status === $status) {
11 return false;
12 }
13
14 $from = $this->status;
15 $this->status = $status;
16 $this->statusChangedAt = $now;
17 $this->events[] = ApplicationEvent::record(ApplicationEventKind::Moved, $actor, ['from' => $from->value, 'to' => $status->value], $now);← історія
18
19 return true;
20 }
21
22 public function withdraw(DateTimeImmutable $now): void
23 {
24 if (! $this->canWithdraw()) {← інваріант
25 throw new InvalidArgumentException('Розгляд цього відгуку завершено: '.$this->outcome?->label().'.');
26 }
27
28 $this->outcome = ApplicationOutcome::Withdrawn;
29 $this->resumePath = null;
30 $this->events[] = ApplicationEvent::record(ApplicationEventKind::Withdrawn, $this->name, [], $now);← історія
31 }
32}
04 · ТИПОВІ ПОМИЛКИ

Як агрегати зазвичай псують

✕ 1

Агрегат на пів бази

Компанія тримає всі вакансії, а ті всі відгуки. Будь-яка зміна вантажить тисячі рядків і блокує сусідів.

Натомість: робіть межу такою малою, як дозволяє інваріант. Вакансія й відгук живуть окремо.

✕ 2

Зміна двох агрегатів в одній транзакції

Відкликали відгук і в тій самій транзакції зменшили лічильник відгуків у вакансії.

Натомість: лічильник оновлює слухач події ApplicationWithdrawn, у власній транзакції.

✕ 3

Сетери на все

setStage() і setOutcome() дозволяють будь-кому обійти правило, і інваріант тримається лише на дисципліні.

Натомість: методи з доменними назвами, як withdraw(), перевіряють правило всередині кореня.

05 · ДЕРЕВО РІШЕНЬ

Чи потрібен вам агрегат

1
Чи є правило, яке має виконуватися для кількох обʼєктів одночасно?
Наприклад: у відгуку з підсумком етап більше не змінюється.
НІ →Вистачить звичайної моделі. Без інваріанта агрегат лише додає церемоній.
2
Чи змінюють ці обʼєкти разом, однією дією користувача?
Перевести на етап і записати історію: так, разом.
НІ →Окремі агрегати. Посилайтесь один на одного за ID і звʼяжіть подіями.
3
Чи можна назвати один обʼєкт, через який іде будь-яка зміна?
Відгук, а не етап чи запис історії.
НІ →Перегляньте межі: схоже, це два агрегати й доменна подія між ними.
ТАК НА ВСІ →Вам потрібен агрегат. Цей обʼєкт і є його корінь.
06 · СЛОВНИК
Інваріант
Правило, яке має бути правдою після будь-якої зміни. Наприклад: у відгуку з підсумком етап більше не змінюється.
Корінь агрегату
Єдиний обʼєкт агрегату
Межа транзакції
Усе, що зберігається атомарно: або цілком, або ніяк. Для агрегату це він сам.
Доменна подія
Запис про те, що сталося в домені, у минулому часі: ApplicationWithdrawn. Інші частини системи реагують на неї окремо.
07 · ПЕРЕВІР СЕБЕ

Питання зі співбесід

Усі питання з архітектури →

Так межі не зливаються: не можна випадково змінити чужий агрегат через навігацію по звʼязках, і кожен агрегат вантажиться й зберігається окремо.

Записати доменну подію й обробити її окремо, у власній транзакції. Якщо потрібна гарантія доставки, подія спершу пишеться в outbox у тій самій транзакції, що й агрегат.