Модуль 28

Repository та Service patterns

Ось готовий урок, створений за твоїм майстер-промптом.


🎓 CS50: Архітектура ПЗ. Repository та Service patterns

Привіт, друзі! Це CS50, і сьогодні ми поговоримо про те, що відрізняє код "студента, який вчора вивчив синтаксис", від коду професійного інженера.

Ми зануримося в тему Repository (Репозиторій) та Service (Сервіс) патернів.


1. 🔥 Вступ: Хаос у ресторані

Уявіть собі дорогий ресторан. Ви сідаєте за столик, підходить офіціант і приймає замовлення: "Стейк середньої просмажки".

А тепер уявіть, що після цього офіціант... 1. Біжить на кухню. 2. Сам бере ніж і починає різати м'ясо. 3. Раптом розуміє, що м'ясо закінчилося, тому біжить на ферму забивати корову. 4. Повертається, смажить стейк, миє тарілку. 5. І нарешті приносить його вам.

Це абсурд, правда? Але саме так виглядає код більшості початківців!

У вашому коді (наприклад, у контролері веб-додатку) часто змішано все: * Отримання HTTP-запиту (Офіціант). * Бізнес-логіка та правила (Шеф-кухар). * SQL-запити до бази даних (Постачальник продуктів).

Питання до вас: Що станеться з цим рестораном (вашим кодом), якщо ми вирішимо змінити постачальника м'яса? Або якщо офіціант захворіє? Весь процес зупиниться, бо все зав'язано в один величезний вузол.

Навіщо нам ці патерни? Щоб розділити відповідальність. * Ми хочемо, щоб код, який працює з базою даних, не знав про HTTP. * А код, який перевіряє бізнес-правила, не дбав про те, чи лежать дані в MySQL, чи в текстовому файлі.


2. 🧠 Теоретична база (Без нудьги)

Давайте розкладемо все по поличках. У нас є три шари (layers).

1. Repository (Комора / Постачальник)

Це шар доступу до даних. * Суть: Він поводиться як колекція об'єктів. Ви кажете йому: "Дай мені користувача з ID 5" або "Збережи цей товар". * Як це працює "під капотом": Репозиторій містить SQL-запити (SELECT, INSERT) або виклики ORM. * Головне правило: Тут немає бізнес-логіки. Репозиторій — це тупий виконавець. Він просто дістає або кладе дані.

2. Service (Шеф-кухар)

Це мозок вашої програми. * Суть: Тут живуть правила. Наприклад: "Якщо користувач купує товар, перевір, чи є він на складі, спиши гроші, надішли email". * Як це працює: Сервіс викликає методи Репозиторія, щоб отримати дані, обробляє їх і повертає результат. * Головне правило: Сервіс не повинен знати, як саме зберігаються дані (SQL чи NoSQL). Він просто каже Репозиторію: "Знайди мені це".

3. Controller / Handler (Офіціант)

  • Приймає запит від клієнта.
  • Викликає Сервіс.
  • Віддає відповідь клієнту (JSON, HTML).

Інтуїтивно: * Repository — це руки, що беруть дані. * Service — це голова, що думає, що з ними робити.


3. 🧪 Приклади (Кодимо!)

Давайте подивимось на прикладі реєстрації користувача (Python-style псевдокод).

❌ Як робити НЕ треба (Spaghetti Code)

Уявіть, що ви відкриваєте файл контролера, а там таке:

def register_user(request):
    username = request.form['username']
    password = request.form['password']

    # ПЕРЕВІРКА (Логіка)
    if len(password) < 8:
        return "Password too short!"

    # РОБОТА З БД (SQL прямо в контролері? Жах!)
    db = connect_to_database()
    cursor = db.cursor()
    cursor.execute("INSERT INTO users (name, password) VALUES (...)")

    # ЩЕ ЛОГІКА
    send_welcome_email(username)

    return "Success!"

Що тут поганого? Якщо ви захочете змінити базу даних, вам доведеться лізти сюди. Якщо ви захочете змінити правила пароля — теж сюди. Цей код неможливо тестувати частинами.


✅ Крок 1: Створюємо Repository

Давайте винесемо роботу з базою даних.

Питання: Що ми очікуємо від класу UserRepository? Тільки методи для збереження та пошуку, так?

class UserRepository:
    def save(self, user):
        # Тут живе "брудний" SQL або ORM
        print(f"DEBUG: Saving {user} to Database via SQL...")
        # db.execute("INSERT INTO...")

    def find_by_email(self, email):
        print(f"DEBUG: SELECT * FROM users WHERE email = {email}")
        return None # або знайдений юзер

Бачите? Жодної перевірки довжини пароля. Тільки дані.


✅ Крок 2: Створюємо Service

Тепер нам потрібен хтось, хто керує процесом.

Питання: Хто буде викликати репозиторій? Правильно, Сервіс.

class UserService:
    def __init__(self, user_repository):
        # Ми передаємо репозиторій всередину (Dependency Injection)
        self.repo = user_repository

    def register(self, username, password):
        # 1. Бізнес-логіка: Валідація
        if len(password) < 8:
            raise Exception("Пароль надто короткий!")

        # 2. Бізнес-логіка: Перевірка унікальності
        if self.repo.find_by_email(username):
             raise Exception("Користувач вже існує!")

        # 3. Делегування збереження репозиторію
        new_user = {"name": username, "pass": password}
        self.repo.save(new_user)

        # 4. Ще логіка
        print(f"Sending email to {username}")

Тепер наш контролер (Офіціант) виглядає чисто і красиво:

# Controller
def register_user(request):
    try:
        service.register(request.form['username'], request.form['password'])
        return "Success"
    except Exception as e:
        return str(e)

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

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

Завдання 1: Знайди помилку У коді нижче в метод Репозиторія закралася помилка архітектури. Знайдіть її.

class ProductRepository:
    def get_product(self, id):
        product = db.query(f"SELECT * FROM products WHERE id={id}")
        # Увага сюди:
        if product.price < 0:
            product.price = 0 # Виправлення ціни
        return product

(Підказка: Чи повинен комірник змінювати цінники, якщо вони йому не подобаються?)

Завдання 2: Створи Service Напишіть клас OrderService. * У нього має бути метод create_order(user_id, item_id). * Він має використовувати ProductRepository (щоб дізнатися ціну) та OrderRepository (щоб зберегти замовлення). * Умова: Якщо ціна товару 0, сервіс має викинути помилку "Товар не для продажу".

Завдання 3: Міні-кейс Клієнт каже: "Ми переїжджаємо з SQL бази даних на прості текстові файли". * Який із ваших класів (Service чи Repository) доведеться переписати повністю? * Чи доведеться змінювати код у Controller?

Завдання 4: А що, якщо... Що, якщо нам потрібно, щоб при створенні замовлення користувачу нараховувалися бонусні бали? В який клас ви допишете цю логіку?


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

Як думає Senior Developer, дивлячись на цей код?

  1. "Тонкі контролери, Товсті сервіси". Новачки пхають все в контролер. Профі залишають контролер пустим — він просто "регулювальник руху", а вся "магія" відбувається в сервісах.

  2. Абстракція — наш друг. Сеньйор думає: "Сьогодні ми пишемо в PostgreSQL, а завтра захочемо писати логи в Redis". Завдяки патерну Repository, Service про це навіть не дізнається. Ми просто підмінимо один клас репозиторія на інший.

  3. Типова помилка: Робити "God Service" (Божественний сервіс). Це коли UserService починає керувати товарами, замовленнями, доставкою і погодою. Розділяйте: UserService для юзерів, OrderService для замовлень.


6. 🧩 Підсумок

Отже, що ми сьогодні зробили? Ми навели лад у нашому "ресторані" коду.

  • Repository: Відповідає за те, де і як беруться дані (SQL, файл, API).
  • Service: Відповідає за те, що з цими даними робити (бізнес-правила).
  • Ви тепер вмієте: Писати код, який легко читати, легко тестувати і легко змінювати.

У наступному уроці ми подивимося, як автоматично зв'язувати ці компоненти, щоб не створювати їх вручну щоразу. Це називається магічним словом Dependency Injection.

А поки що... це був CS50. Успіхів у коді!