Понимание паттерна Фабрика в JavaScript
Вы написали new UserService() в двенадцати разных файлах. Теперь сигнатура конструктора изменилась, и вы охотитесь по всей кодовой базе, исправляя каждый вызов. Именно эту проблему решает паттерн Фабрика.
Паттерн Фабрика абстрагирует создание объектов, предоставляя вам единую точку контроля над тем, как объекты создаются. Это не просто функция, которая возвращает объекты — это паттерн проектирования, который отделяет что вам нужно от того, как это конструируется.
Ключевые выводы
- Паттерн Фабрика централизует создание объектов, упрощая управление изменениями конструкторов по всей кодовой базе.
- Фабричные функции обеспечивают приватность через замыкания, в то время как классы могут обеспечить приватность во время выполнения с помощью
#privateполей и совместно использовать методы через цепочку прототипов. - Комбинирование фабрик с внедрением зависимостей упрощает тестирование, позволяя легко подменять зависимости.
- Используйте фабрики, когда вам нужны решения о типах во время выполнения или сложная логика конструирования — пропускайте их для простого, стабильного создания объектов.
Что на самом деле представляет собой паттерн Фабрика
Фабричная функция возвращает объект. Паттерн Фабрика шире: это архитектурный подход, при котором логика создания существует отдельно от кода использования.
Вот в чём различие:
// Simple factory function
const createUser = (name) => ({ name, createdAt: Date.now() })
// Factory Pattern: abstracts which class to instantiate
function createNotification(type, message) {
const notifications = {
email: () => new EmailNotification(message),
sms: () => new SMSNotification(message),
push: () => new PushNotification(message)
}
if (!notifications[type]) {
throw new Error(`Unknown notification type: ${type}`)
}
return notifications[type]()
}
Паттерн проявляет себя наилучшим образом, когда вам нужно решить, какой объект создать на основе условий времени выполнения, или когда конструирование требует конфигурации, о которой вызывающий код не должен знать.
Фабричные функции JavaScript против классов
Оба подхода имеют своё место. Выбор зависит от того, что вы оптимизируете.
Классы со статическими фабричными методами хорошо работают, когда вы хотите совместного использования методов на основе прототипов и чётких путей инстанцирования:
class ApiClient {
#baseUrl
#authToken
constructor(baseUrl, authToken) {
this.#baseUrl = baseUrl
this.#authToken = authToken
}
static forProduction(token) {
return new ApiClient('https://api.example.com', token)
}
static forDevelopment() {
return new ApiClient('http://localhost:3000', 'dev-token')
}
}
Фабричные функции превосходны, когда вам нужна истинная приватность через замыкания или вы хотите избежать сложности привязки this:
function createCounter(initial = 0) {
let count = initial
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
}
}
Компромисс: фабричные функции создают новые экземпляры методов для каждого объекта. Классы совместно используют методы через цепочку прототипов. Для большинства фронтенд-приложений эта разница незначительна — но она имеет значение, если вы создаёте тысячи экземпляров.
Discover how at OpenReplay.com.
Внедрение зависимостей с фабриками
Фабрики становятся мощными в сочетании с внедрением зависимостей. Вместо жёсткого кодирования зависимостей вы передаёте их в фабрику:
function createUserService({ apiClient, logger, cache }) {
return {
async getUser(id) {
const cached = cache.get(`user:${id}`)
if (cached) return cached
logger.debug(`Fetching user ${id}`)
const user = await apiClient.get(`/users/${id}`)
cache.set(`user:${id}`, user)
return user
}
}
}
// In your application setup
const userService = createUserService({
apiClient: productionApiClient,
logger: consoleLogger,
cache: memoryCache
})
// In tests
const testUserService = createUserService({
apiClient: mockApiClient,
logger: nullLogger,
cache: noOpCache
})
Этот паттерн делает тестирование простым — библиотеки для моков не требуются.
Когда не использовать фабрики
Фабрики добавляют косвенность. Это ценно, когда вам нужна гибкость, но затратно, когда не нужна.
Пропустите фабрику, когда:
- Создание объекта простое и вряд ли изменится
- Вы создаёте только один тип объекта
- Добавленная абстракция скорее затемняет, чем проясняет
Фабрика, оборачивающая единственный вызов new Date(), — это не паттерн, это шум.
Паттерны создания объектов в современных JS-модулях
ES-модули меняют то, как фабрики вписываются в архитектуру приложения. Вы можете экспортировать сконфигурированные фабричные функции напрямую:
// services/notifications.js
import { EmailService } from './email.js'
import { config } from '../config.js'
export const createNotification = (type, message) => {
// Factory has access to module-scoped dependencies
const emailService = new EmailService(config.smtp)
// ... creation logic
}
Граница модуля обеспечивает естественную инкапсуляцию. Потребители импортируют фабрику, не зная её зависимостей.
Заключение
Используйте паттерн Фабрика, когда вам нужно централизовать сложную логику конструирования, менять реализации без изменения вызывающего кода, внедрять зависимости для тестирования или определять типы объектов во время выполнения.
Начинайте с прямого инстанцирования. Вводите фабрики, когда боль от разбросанных вызовов new становится реальной. Паттерн существует для решения проблем — а не для удовлетворения архитектурных идеалов.
Часто задаваемые вопросы
Фабричная функция просто возвращает объект. Паттерн Фабрика — это более широкий архитектурный подход, который отделяет логику создания от кода использования, часто решая, какой тип объекта создать, на основе условий времени выполнения. Паттерн обеспечивает единую точку контроля для инстанцирования объектов во всём приложении.
Используйте фабрики, когда сигнатуры конструкторов могут измениться, когда вам нужно определять типы объектов во время выполнения, когда конструирование включает сложную конфигурацию или когда вы хотите внедрять зависимости для более простого тестирования. Для простых объектов со стабильными конструкторами прямое инстанцирование чище.
Фабричные функции создают новые экземпляры методов для каждого объекта, в то время как классы совместно используют методы через цепочку прототипов. Эта разница незначительна для большинства приложений, но становится существенной при создании тысяч экземпляров. Выбирайте на основе ваших конкретных требований к производительности и потребностей в приватности.
Фабрики в сочетании с внедрением зависимостей позволяют вам передавать мок-зависимости напрямую в фабрику во время тестов. Это устраняет необходимость в библиотеках для моков или сложной настройке тестов. Вы можете заменить реальные API-клиенты, логгеры или кеши тестовыми дублями, просто передав другие аргументы.
Truly understand users experience
See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..