Skip to main content

Що таке патерн Factory (Фабрика)?

Factory (Фабрика) - патерн створення об'єктів, що ховає логіку new за функцією або методом, щоб решта коду не знала, який саме клас інстанціювати.

Теорія

TL;DR

  • Factory централізує new в одному місці; код просить об'єкт за типом, а не за назвою класу
  • Корисний, коли тип об'єкта залежить від рядка конфігурації, ролі користувача або змінної середовища
  • Три форми: Simple Factory (один статичний метод), Factory Method (підклас перевизначає create()), Abstract Factory (сімейство пов'язаних об'єктів)
  • Компроміс: Simple Factory потребує редагування при кожному новому типі; Factory Method вирішує це через підкласи

Швидкий приклад

javascript
// Без фабрики - код знає про кожен конкретний клас const user = role === 'admin' ? new AdminUser(data) : new RegularUser(data); // З фабрикою - код знає тільки про фабрику const user = UserFactory.create(role, data);

Як працює Simple Factory

Simple Factory - це статичний метод або звичайна функція, що приймає рядок-тип і повертає потрібний об'єкт. Жодний конкретний клас не потрапляє у викликаючий код.

javascript
class NotificationFactory { static create(channel, message) { switch (channel) { case 'email': return new EmailNotification(message); case 'sms': return new SMSNotification(message); case 'push': return new PushNotification(message); default: throw new Error(`Невідомий канал: ${channel}`); } } } const notif = NotificationFactory.create('email', 'Привіт!'); notif.send(); // EmailNotification відправляє

Весь switch живе в одному файлі. Додаєш новий канал - змінюєш один клас, а не шукаєш по всій кодовій базі.

Патерн Factory Method

Simple Factory - поширена конвенція, але в книзі Gang of Four його немає. Factory Method - це вже офіційний GoF-патерн. Базовий клас визначає метод на кшталт createProduct(), і кожен підклас перевизначає його, щоб повертати свій тип об'єкта.

javascript
class Dialog { createButton() { throw new Error('createButton must be implemented'); } render() { const button = this.createButton(); // factory method button.onClick(() => this.closeDialog()); button.render(); } } class WindowsDialog extends Dialog { createButton() { return new WindowsButton(); } } class WebDialog extends Dialog { createButton() { return new HTMLButton(); } }

Метод render() у базовому класі не знає, з яким типом кнопки працює. Новий тип платформи - новий підклас. Dialog не чіпаємо.

Коли використовувати

  • Тип об'єкта залежить від значення конфігурації, вибору користувача або змінної середовища
  • Треба замінити реалізацію без зміни коду, що викликає (зручно в тестах: фабрика повертає mock, реальний код не змінюється)
  • Є кілька класів з однаковим інтерфейсом, але різною поведінкою
  • Пишеш бібліотеку, де користувач має підключати власні реалізації

Найпростіший сигнал на практиці: якщо код вже має if/else або switch для вибору класу, ця логіка має жити у фабриці, а не розпорошуватись по фічах.

Не додавай фабрику просто тому, що є два класи. Якщо завжди створюється один і той самий тип, new - правильний вибір.

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

Помилка 1: Фабрика без розгалуження

javascript
// Зайвий рівень абстракції без жодної користі class UserFactory { static create(data) { return new User(data); // завжди один клас } }

Якщо логіка не розгалужується, фабрика нічого не дає.

Помилка 2: Об'єкти з різними інтерфейсами

javascript
class AnimalFactory { static create(type) { if (type === 'dog') return new Dog(); // є .bark() if (type === 'cat') return new Cat(); // є .meow() // код все одно мусить перевіряти тип - фабрика не допомогла } }

Всі об'єкти з фабрики мають підтримувати один інтерфейс. Якщо Dog і Cat мають різний API, код повертається до перевірки типів вручну.

Помилка 3: Немає гілки default

javascript
static create(type) { switch (type) { case 'admin': return new AdminUser(); case 'guest': return new GuestUser(); // немає default - повертає undefined без жодної помилки } }

Завжди кидай помилку при невідомому типі. Мовчазний undefined дає заплутані баги пізніше.

Помилка 4: Плутанина Simple Factory і Factory Method

Це різні речі. Simple Factory - зручна конвенція. Factory Method - формальний GoF-патерн на основі наслідування. На інтерв'ю уточни, що саме мається на увазі, перш ніж відповідати.

Де зустрічається в реальному коді

  • React: React.createElement() - фабрична функція. Передаєш рядок або клас компонента, отримуєш елемент virtual DOM.
  • Node.js http: http.createServer() абстрагує конструктор і повертає Server.
  • Express: express() сам по собі фабрика, що повертає Application.
  • Jest: jest.fn() повертає налаштовану mock-функцію.
  • Angular DI: інжектор використовує фабричні функції для створення сервісів.

Follow-up питання

Q: Яка різниця між Simple Factory і Factory Method?
A: Simple Factory - статичний метод або функція, що обирає клас і викликає new. Factory Method - GoF-патерн, де базовий клас визначає метод, який підкласи перевизначають. Simple Factory потребує редагування одного файлу при новому типі; Factory Method - додавання нового підкласу.

Q: Чи можна писати фабрику без класу?
A: Так, звичайна функція підходить. function createUser(role, data) { ... } - це теж фабрика. Клас нічого не додає, крім простору імен. Багато JavaScript-проєктів обирають прості фабричні функції замість статичних методів.

Q: Як фабрика допомагає в тестах?
A: Фабрику передають як залежність замість прямого виклику new у бізнес-логіці. У тестах підміняють фабрику, щоб вона повертала mock. Код, що тестується, не знає різниці.

Q: Коли обирати Factory Method замість Simple Factory?
A: Коли нові типи з'являються часто, або коли зовнішній код має визначати власні типи без зміни вихідників. Factory Method дозволяє розширювати без редагування наявного коду.

Q: Що таке Abstract Factory і коли він потрібен?
A: Abstract Factory створює сімейства пов'язаних об'єктів. Замість одного методу - кілька методів створення. WindowsUIFactory створює WindowsButton і WindowsScrollbar разом. Використовується, коли об'єкти мають бути узгоджені між собою.

Приклади

Базовий: створення фігур

javascript
class Circle { constructor(radius) { this.radius = radius; } area() { return Math.PI * this.radius ** 2; } } class Rectangle { constructor(w, h) { this.width = w; this.height = h; } area() { return this.width * this.height; } } function createShape(type, ...args) { if (type === 'circle') return new Circle(...args); if (type === 'rectangle') return new Rectangle(...args); throw new Error(`Невідома фігура: ${type}`); } const shapes = [ createShape('circle', 5), createShape('rectangle', 4, 6), ]; shapes.forEach(s => console.log(s.area())); // 78.53... // 24

Обидві фігури мають .area(). Код у forEach не знає конкретного типу за кожним об'єктом.

Практичний: платіжні системи

javascript
class StripeProcessor { pay(amount) { console.log(`Stripe: списуємо $${amount}`); } } class PayPalProcessor { pay(amount) { console.log(`PayPal: відправляємо $${amount}`); } } class CryptoProcessor { pay(amount) { console.log(`Crypto: переказуємо $${amount} у BTC`); } } class PaymentFactory { static create(method) { const map = { stripe: StripeProcessor, paypal: PayPalProcessor, crypto: CryptoProcessor, }; const Processor = map[method]; if (!Processor) throw new Error(`Не підтримується: ${method}`); return new Processor(); } } // Метод приходить з вибору користувача const processor = PaymentFactory.create(getUserSelectedMethod()); processor.pay(99.99);

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

Розширений: фабрика з реєстром

javascript
class PluginFactory { static #registry = new Map(); static register(name, Constructor) { this.#registry.set(name, Constructor); } static create(name, ...args) { const Constructor = this.#registry.get(name); if (!Constructor) throw new Error(`Плагін "${name}" не зареєстрований`); return new Constructor(...args); } } // Вбудовані плагіни реєструються при старті PluginFactory.register('logger', LoggerPlugin); PluginFactory.register('cache', CachePlugin); // Сторонній плагін розширює систему без зміни PluginFactory PluginFactory.register('analytics', AnalyticsPlugin); const logger = PluginFactory.create('logger', { level: 'debug' });

Жодного switch для редагування. Нові типи реєструються самостійно. Цей підхід зустрічається в Webpack loaders, Babel plugins і будь-яких інших системах з підтримкою плагінів.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?