Модуль 16

Сервіси та Dependency Injection

Ось твій урок, створений у стилі Девіда Малана (CS50).


🎓 Урок: Сервіси та Dependency Injection

(Мистецтво не створювати собі проблем)

Привіт, друзі! Сьогодні ми поговоримо про одну з найважливіших концепцій у сучасній розробці. Це той рубікон, який відділяє код "студента, що написав скрипт за вечір" від коду "інженера, який будує систему на роки".

Ми поговоримо про Сервіси та Dependency Injection (DI) — або, простіше кажучи, "Впровадження залежностей".


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

Уявіть, що ви будуєте будинок. Вам потрібне світло. Що ви робите? Ви берете лампочку і вкручуєте її в патрон, так?

А тепер уявіть, що замість патрона ви припаюєте лампочку намертво до дротів у стіні. Працює? Так, світло є. Але що станеться, коли лампочка перегорить? Вам доведеться роздовбати стіну, відрізати дроти та паяти нову лампу.

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

Дивіться на цей код (давайте, наприклад, на TypeScript/JS, бо це зрозуміло всім):

class UserProfile {
    private logger = new ConsoleLogger(); // <--- Оце і є "припаяна лампочка"!

    saveUser() {
        this.logger.log("Зберігаю юзера...");
        // логіка збереження
    }
}

Бачите проблему? Клас UserProfile сам створює собі ConsoleLogger. Вони склеєні намертво. А що, як завтра ваш бос скаже: "Ми більше не пишемо логи в консоль, тепер ми пишемо їх у файл або відправляємо в Telegram"?

Вам доведеться лізти в код UserProfile (і в сотні інших місць, де ви це написали) і переписувати його. Це пекло. Це "спагеті-код".

Риторичне питання: Хіба профіль користувача має хвилюватись про те, як саме і куди пишуться логи? Ні. Його робота — зберігати юзера.

Як же нам зробити так, щоб ми могли міняти "лампочки" (сервіси), не ламаючи "стіни" (компоненти)?


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

Давайте розкладемо це на атоми.

Що таке Сервіс?

Це просто клас (або функція), який робить одну корисну роботу. * Надіслати email? Це EmailService. * Порахувати податки? Це TaxService. * Отримати дані з сервера? Це ApiService.

Головне правило: Сервіс не має знати, хто його використовує. Він просто інструмент.

Що таке Dependency Injection (DI)?

Звучить страшно, але насправді це елементарно. Це коли об'єкт не створює свої залежності сам (не робить new Service()), а отримує їх ззовні.

Аналогія з піцою: 1. Без DI: Ви хочете піцу. Ви йдете на кухню, замішуєте тісто, ріжете ковбасу, печете. Ви залежите від наявності продуктів у вас вдома. 2. З DI: Ви замовляєте доставку. Кур'єр (DI-контейнер) приносить вам готову піцу. Вам байдуже, як її готували. Ви просто її споживаєте.

Що треба запам'ятати:

  1. Inversion of Control (IoC) / Інверсія управління: Замість того, щоб компонент керував створенням сервісів, ми віддаємо це управління комусь іншому (фреймворку або головному файлу програми).
  2. Constructor Injection: Найпопулярніший спосіб. Ми передаємо сервіси через конструктор класу. "Я не працюватиму, поки ти не даси мені мої інструменти".

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

Давайте виправимо наш код крок за кроком.

Етап 1: Жорстка зчіпка (Як НЕ треба)

class ReportGenerator {
    // ЖАХ: Ми жорстко прив'язані до конкретного класу PDFWriter
    private writer = new PDFWriter(); 

    generate() {
        this.writer.write("Звіт готовий");
    }
}

Якщо треба буде генерувати Excel, нам гаплик.

Етап 2: Ручне впровадження (Dependency Injection)

Ми кажемо класу: "Не створюй writer сам. Попроси його в мене".

class ReportGenerator {
    private writer: any; // Поки що any, для простоти

    // МАГІЯ ТУТ: Ми приймаємо інструмент ззовні!
    constructor(writerImplemetation) {
        this.writer = writerImplemetation;
    }

    generate() {
        this.writer.write("Звіт готовий");
    }
}

// Використання:
const pdf = new PDFWriter();
const report1 = new ReportGenerator(pdf); // Впровадили PDF!

const excel = new ExcelWriter();
const report2 = new ReportGenerator(excel); // Впровадили Excel!

Бачите? Ми не змінили жодного рядка в ReportGenerator, але він навчився працювати з різними форматами!

Етап 3: Професійний підхід (Інтерфейси)

Досвідчені розробники не покладаються на any. Вони укладають контракт (інтерфейс).

// 1. Контракт: Що має вміти робити будь-який Writer?
interface IWriter {
    write(text: string): void;
}

// 2. Реалізації: Конкретні виконавці
class PDFWriter implements IWriter {
    write(text: string) { console.log(`🖨️ Друкую в PDF: ${text}`); }
}

class ConsoleWriter implements IWriter {
    write(text: string) { console.log(`💻 Виводжу в консоль: ${text}`); }
}

// 3. Споживач: Йому байдуже, хто саме пише, аби був метод write
class ReportGenerator {
    constructor(private writer: IWriter) {} // "Дайте мені будь-кого, хто вміє писати"

    generate() {
        this.writer.write("Фінансовий звіт за рік");
    }
}

Тепер наш ReportGenerator став універсальним солдатом.


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

Час "забруднити руки". Спробуйте виконати ці завдання (можна подумки або в редакторі коду):

Завдання 1: Рефакторинг У вас є клас Store, який всередині робить this.payment = new PayPalService(). Перепишіть його так, щоб PayPalService передавався через конструктор.

Завдання 2: Створення Mock (Фейка) Уявіть, що ви тестуєте Store із Завдання 1. Ви не хочете списувати реальні гроші під час тестів. Створіть клас FakePaymentService, який просто пише в консоль "Оплата пройшла (понарошку)", і передайте його в Store.

Завдання 3: "А що, якщо..." Додайте в інтерфейс методу оплати метод refund() (повернення коштів). Що станеться з вашим кодом? Де компілятор почне "лаятись" і чому це добре?

Завдання 4: Міні-кейс Ви робите гру. У вас є Hero (Герой). Герой може тримати в руках різну зброю (Sword, Bow, Axe). Як за допомогою DI зробити так, щоб герой міг змінювати зброю "на льоту", не змінюючи код самого класу Hero? Опишіть структуру класів.


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

Послухайте уважно. Коли джуніор бачить задачу "відправити SMS", він пише код відправки прямо в кнопці "Купити".

Коли сеньйор бачить ту ж задачу, він думає: 1. "Сьогодні це SMS, завтра — WhatsApp, післязавтра — поштова сова". 2. "Я винесу логіку в окремий NotificationService". 3. "Я сховаю його за інтерфейсом INotifier".

Типові помилки: * Забагато сервісів: Не створюйте сервіс для додавання двох чисел. Використовуйте здоровий глузд. * Circular Dependency (Замкнене коло): Сервіс А хоче Сервіс Б, а Сервіс Б хоче Сервіс А. Це як "курка і яйце". Програма впаде. Уникайте цього, розділяючи логіку.

Порада від Девіда: Завжди питайте себе: "Як я буду це тестувати?" Якщо ви не можете протестувати клас без підключення до реальної бази даних або інтернету — у вас погана архітектура. DI вирішує цю проблему, дозволяючи підсунути "фейкову" базу даних.


6. 🧩 Підсумок

Отже, що ми сьогодні зробили? 1. Ми розпиляли монолітний код. 2. Ми навчилися впроваджувати залежності, а не створювати їх. 3. Ми зрозуміли силу інтерфейсів.

Тепер ваш код гнучкий, як гімнаст, і надійний, як швейцарський годинник. Ви можете міняти деталі системи, не руйнуючи саму систему.

🔍 Тизер наступного уроку: Зараз ми передавали залежності вручну: new ClassA(new ClassB()). Але що, якщо залежностей 50 штук? Писати це руками — біль. На наступному уроці ми дізнаємось про IoC Containers (Inversion of Control Containers) — роботів, які самі знаходять потрібні сервіси і вставляють їх куди треба. Це справжня магія Angular, NestJS, Spring та .NET!

А поки що — практикуйтесь і не пишіть new всередині класів! До зустрічі! 🚀