Abstract Factory Pattern — Абстрактная фабрика

Фабрика фабрик: семейство связанных объектов под несколько платформ. Универсальный код не должен знать, под какой платформой работает.

Проблема

Приложение должно работать на нескольких платформах (iOS, Android, Web, Desktop) с разными визуальными контролами. Нужно писать клиентский код один раз — он не знает, какие конкретно классы создаёт. Паттерн обеспечивает совместимость создаваемых объектов: если использовали «тёмную тему», то и кнопки, и поля ввода будут тёмными.

Где используется

  • UI-библиотеки: семейство компонентов для разных тем (светлая/тёмная) или платформ (web/native)
  • Кросс-платформенные приложения: Windows vs macOS
  • Базы данных: фабрики для MySQL/PostgreSQL с одинаковым API
  • Тестирование: замена реальных сервисов на mock через фабрику
  • Плагинные системы: динамический выбор набора компонентов

Решение

Один класс с пачкой методов-конструкторов:

  • createButton, createInput, createProgressBar...
  • Каждая платформа — своя реализация фабрики
  • Клиент получает фабрику, а не конкретные классы

Реализации

Классическая (на классах)

class UIFactory {
  createButton { throw new Error('abstract'); }
  createInput { throw new Error('abstract'); }
}

class LightThemeFactory extends UIFactory {
  createButton { return { render:  => '<button class="btn-light">Click</button>' }; }
  createInput { return { render:  => '<input class="input-light" />' }; }
}

class DarkThemeFactory extends UIFactory {
  createButton { return { render:  => '<button class="btn-dark">Click</button>' }; }
  createInput { return { render:  => '<input class="input-dark" />' }; }
}

// Клиентский код — работает с любой фабрикой
function renderForm(factory) {
  const button = factory.createButton;
  const input = factory.createInput;
  return `<form>${input.render}${button.render}</form>`;
}

const theme = localStorage.getItem('theme') === 'dark'
  ? new DarkThemeFactory
  : new LightThemeFactory();
console.log(renderForm(theme));

Функциональный стиль (объект-словарь)

const factories = {
  mysql: {
    createConnection: (cfg) => ({ type: 'mysql', ...cfg }),
    createQuery: (sql) => ({ type: 'mysql', sql }),
  },
  postgres: {
    createConnection: (cfg) => ({ type: 'pg', ...cfg }),
    createQuery: (sql) => ({ type: 'pg', sql: sql.replace('LIMIT', 'FETCH FIRST') }),
  }
};

function getDBFactory(driver) {
  const f = factories[driver];
  if (!f) throw new Error(`Unknown driver: ${driver}`);
  return f;
}

const db = getDBFactory(process.env.DB_DRIVER || 'mysql');

Где используется в JS-экосистеме

  • React Native + платформо-специфичные компоненты
  • Web Components: разные реализации под разные браузеры/режимы
  • Electron vs браузер: разные API доступа к файловой системе
  • Тестовые моки: фабрика mock'ов для test-окружения

Подводные камни

  • Избыточен при одном семействе — если платформа одна и не планируется добавлять — лишний слой.
  • Разрастание фабрик: каждое новое семейство требует новой конкретной фабрики со всеми методами.
  • Смешение Factory Method и Abstract Factory: Factory Method создаёт один продукт через наследование; Abstract Factory — семейство продуктов через композицию.
  • Все методы-конструкторы должны быть согласованы между фабриками (LSP).
  • В JS часто избыточен — динамический выбор класса проще через объект-реестр.

Главные тезисы автора

  • «Несколько семейств, связанных абстракцией» — ключевая фраза.
  • «Универсальный код не должен знать, где он запущен» — главная мотивация.
  • В JS класс — абстракция первого порядка, можно класть в переменную и вызывать new — это упрощает реализацию.

🎓 Источники

См. также