У попередньому матеріалі про момент, коли CRUD уже недостатньо я писала, що DDD стає корисним тоді, коли система вже не просто зберігає дані, а повинна захищати правила.
У статті про межі домену в системі, що росте наступним питанням було: де закінчується одна частина предметної області і починається інша.
Aggregate для мене став наступним кроком у цій логіці.
Найпростіша помилка тут - подивитися на таблиці, побачити зовнішній ключ і вирішити, що пов’язані записи повинні бути одним aggregate.
На практиці це не так. Зв’язок у базі показує, що дані пов’язані. Aggregate відповідає на інше питання: які зміни повинні залишатися узгодженими разом?
База даних легко підказує неправильну межу
products id name status registrations id product_id status access_records id product_id subject_id statusЯкщо дивитися тільки на схему, можна легко сказати: "усе прив’язане до product, отже це один aggregate".
Але схема не показує, які правила мають виконуватися в одну мить.
Вона також не показує, чи registration може жити довше за одну зміну product, чи access має власний життєвий цикл, чи ці частини змінюються з різних причин.
Тому для aggregate я починаю не з foreign key, а з інваріантів.
Перше питання: яке правило я не можу дозволити порушити
Приклад правила: Активний продуктповинен завжди матикоректну конфігурацію. Якщо одна команда змінюєі статус, і конфігурацію,вони належать до одногорішення щодо узгодженості.Якщо система не повинна ніколи бачити стан "product active, але обов’язкової конфігурації немає", ці дві частини мають бути захищені однією зміною.
Це вже аргумент для одного aggregate.
Але якщо інше правило звучить "користувач може отримати доступ до активного product", саме існування доступу не обов’язково має бути частиною тієї самої зміни.
Product може бути активним незалежно від того, скільки access records існує зараз.
Aggregate root є точкою, через яку проходять зміни
final class Product{ public function activate(): void { if (! $this->configuration->isValid()) { throw new DomainException( 'Product is not ready for activation.', ); } $this->status = ProductStatus::ACTIVE; }}Кореневий об’єкт aggregate не потрібен для красивої назви.
Його сенс у тому, що зовнішній код не повинен довільно змінювати внутрішні частини і обходити правила.
Якщо конфігурація належить до того самого aggregate, команда не повинна окремо оновити її в одному місці, а status в іншому, залишивши проміжний недопустимий стан.
Пов’язані сутності можуть бути різними aggregate
Product aggregate Product | +-- configuration +-- lifecycle rules Access aggregate Access | +-- subject +-- validity +-- access rules Це для мене один із найважливіших моментів DDD.
Access може знати ProductId, але не повинен автоматично тягнути всередину весь Product aggregate.
Йому потрібен ідентифікатор, щоб знати, до чого він належить. Йому не обов’язково потрібно володіти життєвим циклом продукту.
Так зв’язок залишається, але межі відповідальності не зникають.
Aggregate визначає межу сильної узгодженості
У межах одного агрегату: завантаження |перевірка правил |зміна стану |збереження |фіксація (commit) Між агрегатами: зміна AВсередині aggregate я очікую сильну узгодженість: після завершення команди його інваріанти мають бути правильними.
Між aggregate ситуація інша. Якщо одна зміна логічно викликає іншу, це вже питання прикладного сценарію і координації.
Іноді обидва aggregate все одно змінюються в одній database transaction. Але навіть тоді вони не зобов’язані перетворюватися на одну модель.
Транзакція і aggregate відповідають на різні питання.
Надто великий aggregate швидко починає заважати
Один величезний агрегат Product +-- configuration +-- registrations +-- access records +-- history +-- settings +-- policies Кожна дрібна зміна:завантажити всезаблокувати всезнати всезберегти всеЯкщо aggregate об’єднати за принципом "усе має product_id", він дуже швидко почне знати забагато.
Навіть проста зміна назви продукту може вимагати завантажувати стан, який стосується доступів, реєстрацій або інших незалежних процесів.
Зростає кількість блокувань, конфліктів і причин, через які один об’єкт змінюється.
Це хороший сигнал, що aggregate описує не одну межу узгодженості, а кілька різних.
Але надто дрібний aggregate теж може ламати правила
Занадто дрібно: ProductStatusProductConfiguration зберігаються незалежно Можливий результат: status = activeconfiguration = invalidМета не в тому, щоб зробити якомога більше маленьких aggregate.
Якщо дві частини разом формують один інваріант, їхнє незалежне збереження створює реальну можливість неправильного стану.
Тому я шукаю не найменшу можливу модель, а найменшу модель, яка все ще може сама захистити своє правило.
Я питаю: хто володіє рішенням
Це просте питання часто швидко прояснює межу.
Хто вирішує, що product можна активувати? Product.
Хто вирішує, що конкретний access можна надати? Access або відповідна модель доступу.
Якщо для відповіді одна модель постійно повинна залізати у внутрішні правила іншої, межа, ймовірно, проведена невдало.
Repository працює з aggregate як з одиницею збереження
$product = $products->get( $productId,); $product->activate(); $products->save( $product,);У цьому підході repository для мене не є просто обгорткою над Eloquent-запитами.
Його практичний сенс у тому, що він завантажує достатній стан aggregate для перевірки правил і зберігає результат як одну модель.
Якщо мені для кожної команди доводиться окремо діставати п’ять незалежних сутностей і вручну збирати правила між ними, це привід ще раз перевірити межу агрегату.
Між aggregate я частіше передаю ідентифікатор, а не цілий об’єкт
final class Access{ public function __construct( private ProductId $productId, private SubjectId $subjectId, private AccessStatus $status, ) { }}Це зменшує випадкову зв’язаність.
Access не отримує можливість змінювати Product лише тому, що між їхніми таблицями є зв’язок.
Якщо для сценарію потрібно перевірити стан Product, прикладний рівень може отримати потрібну інформацію і скоординувати дію.
У AVN Center я прийшла до aggregate через правила, а не через таблиці
Це добре збіглося з досвідом AVN Center.
Спочатку дуже легко мислити через таблиці та зв’язки між ними. Але зі зростанням правил стає важливіше інше: яка частина системи володіє рішенням і який стан вона повинна захистити.
У практичному прикладі з реєстром продуктів я вже показувала один конкретніший крок цієї роботи.
Тут я навмисно залишаю приклад узагальненим: мені важливо показати сам спосіб мислення, а не внутрішню карту AVN Center.
Aggregate і database transaction пов’язані, але це не одне й те саме
Агрегатні відповіді: «Що має залишатися узгодженимяк єдина модель?» Транзакційні відповіді: «Які зміни в базі данихфіксуються або скасовуються разом?»У простому сценарії один aggregate часто справді завантажується, змінюється і зберігається в одній транзакції.
Але я не визначаю aggregate через те, які таблиці зараз випадково оновлюються разом.
Межу визначає правило предметної області. Транзакція є технічним способом надійно зберегти цю зміну.
Як я перевіряю, чи дві сутності справді належать до одного aggregate
Я ставлю кілька простих питань:
- Яке правило буде порушене, якщо ці частини змінити окремо?
- Чи повинна система бачити їхній новий стан одночасно?
- Хто володіє рішенням про зміну?
- Чи одна частина має власний життєвий цикл?
- Чи можна посилатися на іншу частину лише через її ідентифікатор?
- Чи спільна модель змушує завантажувати або блокувати непотрібний стан?
- Чи aggregate захищає один набір інваріантів, а не просто повторює структуру foreign key?
Найбільша зміна для мене - перестати моделювати від таблиць
Таблиці все ще важливі. Індекси, зовнішні ключі та обмеження бази нікуди не зникають.
Але під час моделювання aggregate я більше не ставлю схему БД на перше місце.
Спочатку я формулюю правило. Потім визначаю, який стан потрібен, щоб це правило перевірити і захистити. І лише після цього дивлюся, як цей стан краще зберігати.
Цей порядок дуже допоміг мені не перетворювати кожен SQL-зв’язок на об’єктний граф.
Aggregate визначається правилами, а не стрілками між таблицями
Для мене aggregate став зрозумілим лише тоді, коли я перестала думати про нього як про "групу пов’язаних сутностей".
Точніше визначення для практичної роботи таке: це найменша межа, яка може сама захистити важливий інваріант.
Якщо дві сутності просто мають foreign key, цього недостатньо.
Якщо вони повинні змінюватися разом, щоб правило ніколи не було порушене, тоді між ними вже є сильніший зв’язок.
Я починаю aggregate не з таблиці. Я починаю з питання: що система не має права залишити в неправильному стані?
Foreign key показує зв’язок між даними. Aggregate показує, які правила повинні залишатися узгодженими разом.



