Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Senior: питання на співбесіді з теми «DDD і доменне моделювання»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

CQRS (Command Query Responsibility Segregation) - розділення моделі на дві: одна для змін (команди), інша для читання (запити).

Чому одна модель часто незручна для обох задач:

  • запис потребує агрегатів з поведінкою й інваріантами, нормалізованих даних, транзакцій;
  • читання потребує денормалізованих, плоских даних під конкретний екран: «список замовлень з ім'ям клієнта, кількістю позицій, статусом доставки й останнім коментарем».

Одна модель на обидві задачі або обростає зв'язками й N+1 для читання, або втрачає виразність для запису.

Рівні CQRS - від легкого до радикального:

1. Розділення в коді - найпоширеніше й найкорисніше:

// команди: змінюють стан через агрегати
final class PlaceOrder { public function handle(PlaceOrderData $data): OrderId { /* ... */ } }

// запити: окремі класи, що читають напряму, без моделей домену
final class OrderListQuery
{
    public function get(int $customerId): Collection
    {
        return DB::table('orders')
            ->join('customers', ...)
            ->select(['orders.id', 'customers.name', 'orders.status', ...])
            ->where('orders.customer_id', $customerId)
            ->get();
    }
}

Читання не обмежене формою агрегатів - оптимізований SQL під екран.

2. Окремі моделі читання (read models) - денормалізовані таблиці чи проєкції, що оновлюються подіями: таблиця order_summaries з усім потрібним для списку.

3. Окремі сховища - запис у реляційну базу, читання з Elasticsearch/Meilisearch чи окремої репліки.

Коли CQRS виправданий:

  • складний домен з багатою логікою запису й різноманітними поданнями для читання;
  • велика різниця навантажень: читань у сотні разів більше, і їх треба масштабувати окремо;
  • різні вимоги до даних: пошук, звіти, аналітика;
  • разом з event sourcing - там CQRS практично обов'язковий (стан будується з подій).

Коли шкодить: у простих CRUD-застосунках - подвоєння коду без вигоди. Фаулер прямо попереджає: для більшості систем CQRS додає ризику й складності.

Ціна окремих моделей читання:

  • кінцева узгодженість: користувач зберіг зміну, а список ще показує старе. Інтерфейс має це враховувати;
  • синхронізація: проєкції, що не оновилися через помилку, треба вміти перебудувати;
  • більше рухомих частин.

Практична порада: почати з рівня 1 (окремі класи-запити) - він дає більшість користі майже без ціни.

Докладніше в документації: Martin Fowler: CQRS

Event sourcing - замість поточного стану зберігається послідовність подій, що до нього призвели. Стан обчислюється «програванням» подій.

Звичайно:           accounts: id=7, balance=150

Event sourcing:     AccountOpened(id=7)
                    MoneyDeposited(7, 200)
                    MoneyWithdrawn(7, 80)
                    MoneyDeposited(7, 30)
                    → balance = 150

Події лише додаються - ніколи не змінюються й не видаляються. Виправлення - нова подія (DepositCorrected), а не редагування старої.

Переваги:

  • повна історія й аудит «з коробки»: не лише що зараз, а як і чому так сталося. Природно для фінансів, бухгалтерії, медицини, юридичних процесів;
  • стан на будь-який момент у минулому - програти події до потрібної дати;
  • нові подання даних заднім числом: придумали новий звіт - будуєте проєкцію з усієї історії подій, ніби він існував завжди;
  • налагодження: відтворити точну послідовність, що призвела до помилки;
  • природна інтеграція через події з іншими частинами системи.

Ціна - і вона значна:

  • складність: проєкції для читання (CQRS практично обов'язковий), знімки (snapshots) для агрегатів з тисячами подій, обробка кінцевої узгодженості;
  • еволюція подій: подія, записана три роки тому, має читатися й сьогодні. Зміна структури потребує версіонування й «апкастингу» старих подій;
  • видалення даних: незмінний журнал конфліктує з правом на видалення персональних даних (GDPR). Рішення - шифрування персональних даних у подіях окремим ключем і знищення ключа (crypto-shredding);
  • запити до поточного стану - лише через проєкції; «просто зробити SQL-запит» уже не вийде;
  • команда має розуміти підхід - інакше помилки в проєкціях і подіях дорогі.

Коли варто: домен, де історія змін - сама цінність (рахунки, облік, бронювання, системи з аудитом), або де потрібні складні часові запити.

Коли не варто: CRUD, контентні сайти, більшість типових вебзастосунків. Журнал аудиту (spatie/laravel-activitylog) чи таблиця історії змін дають частину переваг без перебудови архітектури.

У Laravel-екосистемі є готові пакети (наприклад, spatie/laravel-event-sourcing), але вони не знімають концептуальної складності.

Докладніше в документації: Martin Fowler: Event Sourcing

Стратегічне проєктування в DDD починається не з коду, а з питання: які частини бізнесу справді важливі і куди вкладати найкращі сили.

Предметну область ділять на піддомени трьох типів:

1. Основний (core) піддомен - те, що відрізняє бізнес від конкурентів і заробляє гроші. Для сервісу пошуку роботи - алгоритм підбору вакансій і якість даних про них; для маркетплейсу - ціноутворення й рекомендації.

  • сюди - найкращі розробники і найретельніше моделювання;
  • писати самим: готове рішення означає мати те саме, що й конкуренти;
  • тут DDD приносить найбільше користі.

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

  • писати самим, але простіше - без складного моделювання, CRUD цілком підходить;
  • можна доручити менш досвідченій команді чи підряднику.

3. Загальний (generic) піддомен - однаковий для більшості бізнесів: автентифікація, платежі, розсилки, бухгалтерія, пошук, зберігання файлів.

  • купувати чи брати готове: Stripe/LiqPay, Mailgun, Meilisearch, Laravel Fortify, готові CRM;
  • писати самим - марнування ресурсів і ризик зробити гірше, ніж готовий продукт.

Як це впливає на архітектуру:

  • межі контекстів часто проходять по межах піддоменів;
  • антикорупційні шари - навколо загальних піддоменів, щоб модель постачальника не просочувалася в основний;
  • інвестиції в якість (тести, рефакторинг, ретельний дизайн) - насамперед в основний піддомен.

Типові помилки:

  • писати загальні речі самостійно («зробимо свою систему розсилок») - роки роботи над тим, що не дає переваги;
  • купувати основний піддомен - віддати ключову перевагу в чужі руки й обмеження чужого продукту;
  • однаковий рівень ретельності для всього - складна DDD-модель у CRUD-адмінці й поспіх в основній логіці.

Класифікація змінюється з часом: унікальна функція стає загальною, коли ринок наздоганяє; загальна може стати основною, якщо бізнес вирішує конкурувати саме в ній.

Для співбесіди: уміння пояснити, що в конкретному проєкті є основним піддоменом, - показник розуміння бізнесу, а не лише технологій.

Докладніше в документації: Azure: аналіз предметної області

DDD часто сприймають як набір тактичних патернів: репозиторії, фабрики, агрегати, value objects, шари. Перенесені механічно в Laravel-проєкт, вони дають багато коду й мало користі. Прагматичний підхід - брати ідеї, а не церемонії.

Що варто брати майже завжди:

  • єдина мова: назви моделей, методів, подій і енумів - зі словника бізнесу ($order->cancel(), OrderCancelled, OrderStatus::Refunded);
  • поведінка поруч з даними для важливих правил: методи на моделях замість $order->status = ... по всьому коду;
  • енуми й об'єкти-значення для грошей, статусів, періодів - через касти Eloquent;
  • доменні події для побічних ефектів;
  • actions/use cases - один клас на бізнес-операцію (PlaceOrder, RefundPayment): зрозуміла точка входу, легко тестувати;
  • модулі за предметними областями, а не за технічними шарами, коли проєкт росте:
app/Domain/Billing/{Models,Actions,Events,Enums}
app/Domain/Catalog/...
app/Domain/Shipping/...

Що варто брати лише для складного ядра:

  • агрегати з явними межами і заборона змінювати дочірні сутності напряму;
  • окремі моделі читання (CQRS) для важких звітів і списків;
  • антикорупційні шари навколо зовнішніх інтеграцій.

Що зазвичай зайве в Laravel-проєкті:

  • репозиторії-обгортки над Eloquent «для чистоти» - дублюють Eloquent і не дають ізоляції;
  • окремі доменні класи + мапінг у Eloquent для простих сутностей - десятки класів перетворень;
  • повна гексагональна архітектура для CRUD-адмінки;
  • event sourcing без бізнес-потреби в історії.

Як вирішувати, де межа: спершу визначити основний піддомен (те, що приносить гроші й має складні правила). Там - більше моделювання й захисту інваріантів. Допоміжні й загальні частини - звичайний Laravel: моделі, Form Request, ресурси, Filament.

Ознаки, що складність виправдана:

  • бізнес-правила часто змінюються і їх багато;
  • помилки дорогі (гроші, юридичні наслідки);
  • над кодом працює кілька команд.

Ознаки «DDD заради DDD»: більше коду інфраструктури, ніж бізнес-логіки; кожна проста зміна торкається п'яти шарів; нові розробники тижнями не розуміють, куди писати код.

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

Докладніше в документації: Martin Fowler: Domain Driven Design