Skip to main content

Патерн абстрактна фабрика

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

Теорія

TL;DR

  • Уяви план заводу, який виробляє комплекти: двигун + колеса + сидіння, де всі деталі підходять одна до одної. Заміни план - отримаєш інший, але теж узгоджений комплект.
  • Головна відмінність від Factory Method: Abstract Factory створює ціле сімейство продуктів, Factory Method - один продукт.
  • Використовуй коли потрібно змінювати сімейства продуктів повністю: теми UI, платформозалежні віджети, драйвери бази даних для різних середовищ.
  • Одна фабрика = одне узгоджене сімейство. Клієнтський код ніколи не змішає Windows-кнопку з Mac-чекбоксом.

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

typescript
interface Button { render(): void; } interface Checkbox { render(): void; } interface GUIFactory { createButton(): Button; createCheckbox(): Checkbox; } class WindowsFactory implements GUIFactory { createButton(): Button { return { render: () => console.log('WindowsButton') }; } createCheckbox(): Checkbox { return { render: () => console.log('WindowsCheckbox') }; } } // Обираєш одну фабрику - отримуєш узгоджену сім'ю const factory: GUIFactory = new WindowsFactory(); factory.createButton().render(); // WindowsButton factory.createCheckbox().render(); // WindowsCheckbox

Один виклик фабрики фіксує все сімейство. Ніякого випадкового змішування частин з різних сімей.

Ключова відмінність від Factory Method

Factory Method - це один метод, який створює один продукт, зазвичай перевизначений у підкласі. Abstract Factory - це інтерфейс з кількома методами, кожен з яких створює різний продукт одного сімейства. Клієнт обирає фабрику один раз, і всі наступні виклики залишаються узгодженими. Саме в цій узгодженості вся суть.

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

  • Кілька продуктів, які мають відповідати одне одному: теми UI, де кнопка, поле введення і модалка потребують однакового стилю.
  • Конфігурація середовища змінює сімейство: dev використовує базу в пам'яті, prod - PostgreSQL. Одна заміна фабрики змінює всі пов'язані об'єкти.
  • Абстракція на рівні фреймворку: платформозалежні віджети. Java AWT робить саме це через Toolkit.getDefaultToolkit().
  • Якщо варіант продукту тільки один - достатньо Factory Method. Abstract Factory додає класи, і це варто виправдовувати.

Як це працює всередині

У TypeScript і Java посилання на фабрику є абстрактним під час компіляції. Рантайм вирішує фактичні виклики методів через vtable, відправляючи до конкретної фабрики залежно від того, який екземпляр присвоєно. Жодного прив'язування до конкретних класів продуктів на етапі компіляції. Заміни екземпляр фабрики через конфіг або dependency injection - і всі продукти зміняться автоматично.

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

Окремі фабрики для кожного продукту

typescript
// Неправильно: окремі фабрики не гарантують узгодженість class ButtonFactory { createButton(): Button { /* windows */ } } class CheckboxFactory { createCheckbox(): Checkbox { /* mac */ } } // Ніщо не заважає змішати Windows-кнопку з Mac-чекбоксом

Всі члени сімейства мають бути в одній фабриці. Обмеження сімейства діє тільки коли всі продукти йдуть з одного місця.

Звернення до конкретних класів у клієнтському коді

typescript
// Неправильно: клієнт знає конкретний клас const btn = new WindowsButton(); // Руйнує абстракцію // Правильно: завжди через фабрику const btn = factory.createButton();

Як тільки клієнтський код знає про WindowsButton, заміна сімейства вимагає змін у цьому коді. А це якраз те, чого патерн мав позбавити.

Надмірна абстракція для малих застосунків

Десять класів для двох кнопок - це порушення YAGNI. Abstract Factory окупається коли є два або більше сімейств з двома або більше продуктами кожне. Для простого перемикання між варіантами вистачить звичайного if/else.

Синглтон-фабрика кешує застарілий стан

Фабрика на рівні модуля може закешувати неправильне сімейство після гарячого перезавантаження або зміни конфігу. Бачив як це підклало команду в Node.js: один воркер закешував темну тему, і вона просочилась у наступні запити після оновлення конфігурації. Свіжий екземпляр на контекст вирішив проблему. Якщо фабрика зберігає конфігураційний стан - не створюй її один раз при старті і не шар між усіма.

Де зустрічається на практиці

  • Java AWT: Toolkit.getDefaultToolkit() повертає платформозалежну фабрику, яка створює WindowsButton, MotifMenu та інші - клієнтський код не знає, на якій ОС працює.
  • React Native: платформоспецифічні UI-кіти використовують цей підхід, щоб iOSButton і AndroidButton виходили через один інтерфейс.
  • Kubernetes client: фабричний підхід для конфігурацій кластера, in-cluster vs out-of-cluster, щоб один і той самий код працював і всередині, і ззовні.
  • gRPC: фабрики каналів перемикаються між HTTP/2 і Unix socket залежно від середовища розгортання.

Питання на співбесіді

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

Q: Коли Abstract Factory шкодить продуктивності?
A: Саме створення фабрики коштує мало. Проблема виникає коли новий екземпляр фабрики створюється на кожен виклик. Там де сімейство не змінюється між запитами - кешуй фабрику як синглтон.

Q: Чим Abstract Factory відрізняється від патерну Prototype?
A: Prototype клонує наявні об'єкти. Abstract Factory створює нові екземпляри через фабричні методи. Prototype підходить коли створення дороге і нові об'єкти схожі на вже наявні. Abstract Factory - про узгодженість сімейства, не про клонування.

Q: Чи може Abstract Factory спричинити циклічні залежності?
A: Так. Якщо фабрика A створює об'єкт B, а B намагається щось створити через A - отримаєш цикл. Рішення: нехай фабрика сама керує повним циклом і послідовністю створення внутрішньо. Продукти не повинні тримати посилання назад на фабрику.

Q: Як розвинути Abstract Factory для підтримки плагінів без змін у клієнтському коді?
A: Через реєстр фабрик (factory registry) з динамічним завантаженням. У Java ServiceLoader знаходить реалізації фабрик під час виконання. Клієнт викликає AbstractFactory.getInstance(config), реєстр вирішує яку конкретну фабрику завантажити. Додати плагін - додати клас фабрики, без змін у клієнтському коді. "Просто додай if/else" - відповідь джуніора. Senior-відповідь - реєстр і динамічна резолюція.

Приклади

Базовий: GUI фабрика для Windows і Mac

typescript
interface Button { render(): void; } interface Checkbox { toggle(): void; } interface GUIFactory { createButton(): Button; createCheckbox(): Checkbox; } class MacFactory implements GUIFactory { createButton(): Button { return { render: () => console.log('Mac button') }; } createCheckbox(): Checkbox { return { toggle: () => console.log('Mac checkbox') }; } } class WinFactory implements GUIFactory { createButton(): Button { return { render: () => console.log('Win button') }; } createCheckbox(): Checkbox { return { toggle: () => console.log('Win checkbox') }; } } function renderUI(factory: GUIFactory) { factory.createButton().render(); factory.createCheckbox().toggle(); } renderUI(new MacFactory()); // Mac button, Mac checkbox renderUI(new WinFactory()); // Win button, Win checkbox

renderUI не знає і не цікавиться платформою. Заміни фабрику - зміниш весь UI. Клієнтський код залишається незмінним.

Середній: React тема-фабрика з визначенням переваг ОС

typescript
interface ThemeButton { render(): JSX.Element; } interface ThemeInput { render(): JSX.Element; } interface ThemeFactory { createButton(): ThemeButton; createInput(): ThemeInput; } class DarkThemeFactory implements ThemeFactory { createButton(): ThemeButton { return { render: () => <button style={{ background: 'black', color: 'white' }}>Натиснути</button> }; } createInput(): ThemeInput { return { render: () => <input style={{ background: '#333', color: 'white' }} /> }; } } class LightThemeFactory implements ThemeFactory { createButton(): ThemeButton { return { render: () => <button style={{ background: 'white', color: 'black' }}>Натиснути</button> }; } createInput(): ThemeInput { return { render: () => <input style={{ background: '#fff', color: 'black' }} /> }; } } const App: React.FC = () => { const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const factory: ThemeFactory = isDark ? new DarkThemeFactory() : new LightThemeFactory(); return ( <div> {factory.createButton().render()} {factory.createInput().render()} </div> ); };

Вибір фабрики відбувається один раз при ініціалізації компонента. Всі елементи теми виходять з однієї фабрики, тому завжди відповідають одне одному. Ніякої ручної координації.

Просунутий: Dependency injection з контролем lifecycle

typescript
interface Logger { log(msg: string): void; } interface Database { query(sql: string): string; } interface AppFactory { createLogger(): Logger; createDatabase(): Database; } class ProdFactory implements AppFactory { createLogger(): Logger { return { log: (msg) => console.log(`[PROD] ${msg}`) }; } createDatabase(): Database { const logger = this.createLogger(); // Фабрика сама керує послідовністю return { query: (sql) => { logger.log(`Виконую: ${sql}`); return 'results'; } }; } } const factory = new ProdFactory(); const db = factory.createDatabase(); db.query('SELECT *'); // [PROD] Виконую: SELECT *

Database потребує Logger, але циклічної залежності (circular dependency) немає: фабрика сама контролює послідовність створення. Якби ці об'єкти збирались вручну поза фабрикою, заміна ProdFactory на DevFactory вимагала б змін у всьому коді збирання. Саме тут Abstract Factory виправдовує себе в реальних проектах.

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

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

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

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