Що таке патерн Factory (Фабрика)?
Factory (Фабрика) - патерн створення об'єктів, що ховає логіку new за функцією або методом, щоб решта коду не знала, який саме клас інстанціювати.
Теорія
TL;DR
- Factory централізує
newв одному місці; код просить об'єкт за типом, а не за назвою класу - Корисний, коли тип об'єкта залежить від рядка конфігурації, ролі користувача або змінної середовища
- Три форми: Simple Factory (один статичний метод), Factory Method (підклас перевизначає
create()), Abstract Factory (сімейство пов'язаних об'єктів) - Компроміс: Simple Factory потребує редагування при кожному новому типі; Factory Method вирішує це через підкласи
Швидкий приклад
// Без фабрики - код знає про кожен конкретний клас
const user = role === 'admin' ? new AdminUser(data) : new RegularUser(data);
// З фабрикою - код знає тільки про фабрику
const user = UserFactory.create(role, data);Як працює Simple Factory
Simple Factory - це статичний метод або звичайна функція, що приймає рядок-тип і повертає потрібний об'єкт. Жодний конкретний клас не потрапляє у викликаючий код.
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(), і кожен підклас перевизначає його, щоб повертати свій тип об'єкта.
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: Фабрика без розгалуження
// Зайвий рівень абстракції без жодної користі
class UserFactory {
static create(data) {
return new User(data); // завжди один клас
}
}Якщо логіка не розгалужується, фабрика нічого не дає.
Помилка 2: Об'єкти з різними інтерфейсами
class AnimalFactory {
static create(type) {
if (type === 'dog') return new Dog(); // є .bark()
if (type === 'cat') return new Cat(); // є .meow()
// код все одно мусить перевіряти тип - фабрика не допомогла
}
}Всі об'єкти з фабрики мають підтримувати один інтерфейс. Якщо Dog і Cat мають різний API, код повертається до перевірки типів вручну.
Помилка 3: Немає гілки default
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 разом. Використовується, коли об'єкти мають бути узгоджені між собою.
Приклади
Базовий: створення фігур
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 не знає конкретного типу за кожним об'єктом.
Практичний: платіжні системи
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, нічого більше.
Розширений: фабрика з реєстром
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 і будь-яких інших системах з підтримкою плагінів.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.