Модуль 29

Domain-driven design та ORM

Ось твій урок. Приготуйся, зараз ми зануримось у світ архітектури та даних!


🎓 УРОК: Domain-Driven Design (DDD) та ORM

Або як перестати бути просто "писарем SQL-запитів" і стати Архітектором Системи


1. 🔥 Вступ: проблема та мотивація

Уявіть, що ви будуєте систему для великого банку. Ваше завдання — переказати гроші з рахунку А на рахунок Б.

Як це зробить новачок? Швидше за все, він напише щось таке прямо в коді обробки кнопки "Надіслати":

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

Здається, працює? Так. Але що, якщо на рахунку №1 недостатньо коштів? А що, якщо банк ввів комісію 1% для переказів у вихідні дні? А якщо валюти різні?

Ви починаєте додавати IF та ELSE навколо цих SQL-запитів. Ваш код перетворюється на спагеті. І найгірше — ця логіка "розмазана" по всьому проєкту. Якщо завтра банк змінить правила комісії, вам доведеться шукати ці SQL-запити в 50 різних файлах. Це пекло, правда?

Питання до вас: Ви хочете керувати бізнесом чи просто пересувати байти в таблицях?

Ось тут на сцену виходить DDD (Domain-Driven Design) — підхід, де головним є не база даних, а Бізнес-Логіка. А щоб нам не доводилося писати SQL вручну для кожного об'єкта, ми використовуємо ORM (Object-Relational Mapping).

Уявіть, що DDD — це шеф-кухар, який знає рецепт ідеальної лазаньї (бізнес-правила), а ORM — це його помічник, який знає, де саме в холодильнику (базі даних) лежать інгредієнти, і приносить їх кухарю. Без кухаря це просто набір продуктів. Без помічника кухар витрачатиме весь час на біганину до складу.


2. 🧠 Теоретична база (без сухої академічності)

Давайте розберемо це "під капотом".

Що таке DDD (Domain-Driven Design)?

Це філософія, яка каже: "Спочатку змоделюй реальний світ у коді, а вже потім думай, як це зберегти".

У центрі DDD є Доменна Модель. Вона складається з:

  1. Сутності (Entities): Об'єкти, які мають унікальний ідентифікатор (ID).
    • Приклад: User (Користувач). Навіть якщо він змінить ім'я, це той самий користувач, бо ID той самий.
  2. Об'єкти-значення (Value Objects): Об'єкти, які визначаються своїми властивостями, а не ID. Вони незмінні (immutable).
    • Приклад: Money (Гроші) або Address. 100 гривень — це завжди 100 гривень. Нам байдуже, яка це саме купюра. Якщо адреса змінилася — це вже нова адреса.
  3. Агрегати (Aggregates): Група об'єктів, які живуть і помирають разом.
    • Приклад: Order (Замовлення) та OrderItem (Товар у замовленні). Немає сенсу зберігати рядок замовлення, якщо саме замовлення видалено. Головна сутність (Order) — це "Корінь Агрегату".

Що таке ORM?

Це "магічний перекладач" між об'єктним світом (Java, C#, Python, PHP) та реляційним світом (SQL таблиці).

  • У коді у вас є Клас User.
  • У базі даних у вас є Таблиця users.
  • У коді: user.save().
  • ORM "під капотом": INSERT INTO users (...) VALUES (...).

📌 Що треба запам’ятати залізно: * DDD відповідає за поведінку (правила гри). * ORM відповідає за збереження (стан гри). * Ніколи не дозволяйте деталям бази даних (наприклад, типам колонок) диктувати вашу бізнес-логіку!


3. 🧪 Приклади (від простого до реального)

Приклад 1: "Анемічна" модель (Як робити НЕ треба)

Новачки часто створюють класи, які просто зберігають дані. Це називається "Anemic Domain Model" (недокрівна модель). Вона слабка.

# Це просто контейнер для даних
class Order:
    id: int
    status: str
    total: float

# Вся логіка десь ззовні (в контролері або сервісі)
order = Order()
order.status = "PAID" # Ми можемо вліпити сюди що завгодно!
order.save()

Що тут не так? Ви можете випадково встановити статус PAID для замовлення з сумою 0, або перевести статус з DELIVERED назад у NEW. Об'єкт не захищає себе.

Приклад 2: "Багата" модель (DDD Way)

Давайте додамо мозку нашому об'єкту.

class Order:
    def __init__(self, id, items):
        self.id = id
        self.items = items
        self.status = "NEW"
        self._total = self.calculate_total()

    # Логіка інкапсульована всередині!
    def pay(self, amount):
        if self.status == "PAID":
            raise Exception("Замовлення вже оплачене!")
        if amount < self._total:
            raise Exception("Недостатньо коштів!")

        self.status = "PAID"
        # Тут може бути подія: надіслати email, тощо.

    # Ми не даємо змінювати статус напряму, тільки через методи

Тепер, коли ви використовуєте order.pay(100), ви гарантовано виконуєте всі бізнес-правила.

Приклад 3: Підключаємо ORM

Як це виглядає з ORM (наприклад, у стилі TypeORM, Hibernate або Entity Framework)?

# Ми помічаємо клас декораторами (анотаціями), щоб ORM знала, куди це класти
@Entity(tableName="orders")
class Order:
    @PrimaryColumn()
    id: int

    @Column()
    status: str

    # ORM сама зрозуміє, як це зберегти

    def pay(self):
        # ... логіка оплати ...
        pass

# Використання:
order_repo = connection.getRepository(Order)
my_order = order_repo.findById(1) # ORM робить SELECT
my_order.pay()                    # DDD логіка (зміна стану в пам'яті)
order_repo.save(my_order)         # ORM робить UPDATE (визначає, що змінилося)

Чому результат саме такий? Ми розділили обов'язки. Клас Order думає про правила торгівлі. Repository (частина ORM) думає про SQL.


4. 🛠 Практична частина

Час розім'яти пальці! Уявіть, що ви проєктуєте систему для Онлайн-Бібліотеки.

Завдання 1: Сутність чи Значення? У вас є поняття ISBN (унікальний код книги) та Book (сама книга). * Хто з них Entity? * Хто з них Value Object? * Чому?

Завдання 2: Рефакторинг (Виправ помилку) У нас є код:

user.email = "невалідний-імейл"
user.save()

Перепишіть клас User так, щоб неможливо було змінити email на некоректний. Створіть метод changeEmail(newEmail), який містить перевірку.

Завдання 3: Агрегати У вас є LibraryCard (Читацький квиток) і Loan (Запис про видачу книги). Чи може Loan існувати без LibraryCard? Як ви організуєте збереження: окремо кожен Loan чи зберігатимете LibraryCard разом зі списком позик?

Завдання 4: Міні-кейс Користувач хоче взяти книгу. Але у нього штраф за прострочення попередньої книги. Напишіть псевдокод методу user.borrow(book), який реалізує це правило.

Завдання 5: А що, якщо... (N+1 Problem) Ви використовуєте ORM. Ви хочете вивести список усіх читачів і назви книг, які вони зараз читають. Ви робите:

users = userRepo.findAll() # 1 запит
for user in users:
    print(user.currentBook.title) # О ні! Це ще N запитів до бази!

Як це виправити засобами ORM? (Підказка: шукайте слово JOIN або Eager Loading).


5. 💡 Мислення як у розробника

Як відрізнити новачка від сеньйора в цій темі?

  1. Помилка новачка: Він намагається відобразити базу даних 1-в-1 у класи. Якщо в базі є колонка foreign_key_id, він створює поле foreign_key_id в класі.

    • Як думає профі: У класі він створює поле Author author, а не int authorId. ORM сама розбереться з ID. Ми працюємо з об'єктами, а не цифрами.
  2. Помилка новачка: Він використовує ORM для складних аналітичних звітів і скаржиться, що це повільно.

    • Як думає профі: ORM — для запису та роботи з бізнес-логікою. Якщо треба зробити складний звіт на мільйон рядків — профі напише чистий, оптимізований SQL або використає Read Model. Не забивайте цвяхи мікроскопом.
  3. Порада з практики (Ubiquitous Language): Використовуйте в коді ті самі слова, що і бізнес. Якщо менеджери кажуть "списати товар", а не "видалити запис", то і метод має називатися writeOff(), а не delete(). Код має читатися як інструкція.


6. 🧩 Підсумок

Отже, що ми сьогодні зробили? Ми перестали бути рабами таблиць і стали володарями сутностей.

  • DDD допомагає нам написати код, який відображає реальний світ і захищає бізнес-правила.
  • ORM звільняє нас від рутинного написання INSERT та UPDATE, дозволяючи зосередитися на логіці.

Тепер ви вмієте не просто "зберігати дані", а моделювати поведінку системи.

Наступного разу: Ми поговоримо про те, як це все протестувати. Адже якщо логіка всередині об'єкта — її дуже легко покрити Unit-тестами! А також зачепимо тему Dependency Injection, щоб наш код став ще гнучкішим.

А поки що — спробуйте переписати свій старий проєкт, сховавши логіку всередину класів. Це змінить ваше мислення назавжди.

Це був CS50... тобто, це був вступ до DDD. Побачимось! 👋