Ось твій урок. Приготуйся, зараз ми зануримось у світ архітектури та даних!
🎓 УРОК: 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 є Доменна Модель. Вона складається з:
- Сутності (Entities): Об'єкти, які мають унікальний ідентифікатор (ID).
- Приклад:
User(Користувач). Навіть якщо він змінить ім'я, це той самий користувач, бо ID той самий.
- Приклад:
- Об'єкти-значення (Value Objects): Об'єкти, які визначаються своїми властивостями, а не ID. Вони незмінні (immutable).
- Приклад:
Money(Гроші) абоAddress. 100 гривень — це завжди 100 гривень. Нам байдуже, яка це саме купюра. Якщо адреса змінилася — це вже нова адреса.
- Приклад:
- Агрегати (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 у класи. Якщо в базі є колонка
foreign_key_id, він створює полеforeign_key_idв класі.- Як думає профі: У класі він створює поле
Author author, а неint authorId. ORM сама розбереться з ID. Ми працюємо з об'єктами, а не цифрами.
- Як думає профі: У класі він створює поле
-
Помилка новачка: Він використовує ORM для складних аналітичних звітів і скаржиться, що це повільно.
- Як думає профі: ORM — для запису та роботи з бізнес-логікою. Якщо треба зробити складний звіт на мільйон рядків — профі напише чистий, оптимізований SQL або використає Read Model. Не забивайте цвяхи мікроскопом.
-
Порада з практики (Ubiquitous Language): Використовуйте в коді ті самі слова, що і бізнес. Якщо менеджери кажуть "списати товар", а не "видалити запис", то і метод має називатися
writeOff(), а неdelete(). Код має читатися як інструкція.
6. 🧩 Підсумок
Отже, що ми сьогодні зробили? Ми перестали бути рабами таблиць і стали володарями сутностей.
- DDD допомагає нам написати код, який відображає реальний світ і захищає бізнес-правила.
- ORM звільняє нас від рутинного написання
INSERTтаUPDATE, дозволяючи зосередитися на логіці.
Тепер ви вмієте не просто "зберігати дані", а моделювати поведінку системи.
Наступного разу: Ми поговоримо про те, як це все протестувати. Адже якщо логіка всередині об'єкта — її дуже легко покрити Unit-тестами! А також зачепимо тему Dependency Injection, щоб наш код став ще гнучкішим.
А поки що — спробуйте переписати свій старий проєкт, сховавши логіку всередину класів. Це змінить ваше мислення назавжди.
Це був CS50... тобто, це був вступ до DDD. Побачимось! 👋