Паттерны проектирования в JavaScript: модуль и фабрика — практическое руководство

Что такое паттерн проектирования в JavaScript
Паттерн проектирования — это повторяемое решение распространённой задачи в программировании. В JavaScript паттерны помогают:
- структурировать код;
- уменьшать связанность компонентов;
- улучшать повторное использование и тестируемость.
Кратко: паттерн — это не библиотека, а способ организации кода. Он может применяться к отдельным функциям, модулям или архитектуре всего приложения.
Important: паттерн не заменяет архитектуру — он дополняет её. Выбор паттерна зависит от требований к инкапсуляции, масштабируемости и простоте тестирования.
Модульный паттерн
Модульный паттерн пригоден, когда нужно закрыть часть реализации и выставить ограниченный публичный API. В классическом ES5-стиле это достигается через IIFE (Immediately Invoked Function Expression).
Базовая структура модуля
(function(){
// Declare private variables and functions
// Declare and return public variables and functions
})();Здесь есть две зоны:
- приватная — локальные переменные и функции, недоступные извне;
- публичная — объект, возвращаемый из IIFE, который содержит методы и поля, доступные внешнему коду.
Плюсы модульного паттерна:
- инкапсуляция состояния;
- предотвращение загрязнения глобальной области видимости;
- явный контракт через возвращаемый объект.
Минусы и ограничения:
- в классическом варианте обновление кода в рантайме затруднено (статический IIFE);
- сложнее писать модульные тесты без явного экспорта (в ES6 это решается экспоротом);
- при неправильной организации публичного API вы можете нечаянно открыть слишком многое.
Пример: модуль UIController
const UIController = (function() {
// the private section of the module
let component = 'Replacement Text';
const changeComponent = function() {
// change all the h1 text to what is in the component variable above
const element = document.querySelector('h1');
element.textContent = component;
}
// the public section of the module
return {
callChangeComponent: function() {
changeComponent();
}
}
})();Здесь changeComponent и component — приватные. Внешний код не может вызвать UIController.changeComponent(), но может вызвать UIController.callChangeComponent(), который косвенно вызывает приватную функцию.
Неправильный вызов (ошибка):
UIController.changeComponent();Правильный вызов:
UIController.callChangeComponent();Когда лучше использовать модуль
- когда нужно скрыть состояние (например, счётчики, кэш);
- для организации логики контроллеров в небольших приложениях;
- когда нужно ограничить API и предотвратить случайные изменения.
Когда модуль не подходит
- если нужен tree-shaking и интерактивный импорт/экспорт — предпочтительнее ES6-модули (import/export);
- если требуется наследование и полиморфизм в классическом OOP-стиле — удобнее классы и фабрики;
- если нужна лёгкая мокируемость в тестах без реэкспорта — применяйте dependency injection.
Notes: современные проекты чаще используют ES-модули (export / import) вместо IIFE. Они дают те же преимущества инкапсуляции, но в стандартной форме и с поддержкой инструментов сборки.
Паттерн фабрика
Фабрика (Factory / Factory Method) полезна, когда приложение должно создавать много похожих объектов с небольшими различиями: разные типы задач, уведомлений, элементов UI и т. п.
Основная идея: централизовать процесс создания объектов в функции/объекте, чтобы не дублировать логику создания по всему коду.
Пример фабрики для задач
// Factory pattern function
const TaskFactory = function() {
this.createTask = function(name, type) {
let task;
// check the type the user selected
if (type === 'urgent') {
task = new UrgentTask(name);
} else if(type === 'trivial') {
task = new TrivialTask(name);
}
// set the type selected in the if statement to the one received as a property
task.type = type;
// used to print the task and its type to the console
task.define = function() {
console.log(`${this.name} (${this.type}): ${this.priority}`)
}
return task
}
}И определения конкретных типов:
// Create the urgent task type
const UrgentTask = function(name) {
this.name = name;
this.priority = "as soon as possible"
}
// create the trivial task type
const TrivialTask = function(name) {
this.name = name;
this.priority = "when you can"
}Для хранения задач в примере используют простую структуру — массив:
// create an array to host the different task
const task = [];Создание задач через фабрику:
// create two new tasks
const factory = new TaskFactory();
task.push(factory.createTask('Clean the house', 'urgent'));
task.push(factory.createTask('Reach level 30 in Candy Crush', 'trivial'));Вывести их в консоль:
// print each task to the console
task.forEach(function(task){
task.define();
});Ожидаемый вывод в консоли:
Clean the house (urgent): as soon as possible
Reach level 30 in Candy Crush (trivial): when you canПочему фабрика полезна
- концентрирует логику создания объектов в одном месте;
- упрощает добавление новых типов (достаточно добавить ветку или карту типов);
- облегчает тестирование производства объектов;
- позволяет применять стратегии создания (кеширование, пул объектов).
Ограничения фабрики
- фабрика может превратиться в «бога» (God Object), если в ней сконцентрировать слишком много логики;
- при большом количестве типов лучше использовать регистр типов (map) вместо цепочки if/else;
- если объекты сложные, лучше применять билдер (Builder) или композицию.
Альтернативные подходы и эволюция паттернов
ES6-модули (import / export)
- позволяют явно экспортировать API и импортировать зависимости;
- дают преимущества сборщиков и tree-shaking.
Классы и наследование (ES6 class)
- полезны при необходимости наследования и переопределения поведения;
- классы совместимы с фабричным подходом (фабрика может возвращать экземпляры классов).
Функции-фабрики (factory functions)
- вместо new возвращают объект; легче тестировать и комбинировать с декораторами.
Dependency Injection
- передаёт зависимости извне, что улучшает тестируемость и гибкость.
Выбор зависит от требований: простота и изоляция — модуль/ES6-модуль; разные экземпляры — фабрика; сложное построение — билдер.
Ментальные модели и эвристики выбора
- «Инкапсуляция» → выбирайте модуль / ES6-модуль.
- «Много похожих объектов» → выбирайте фабрику.
- «Нужно наследование/полиморфизм» → классы + фабрика/абстрактная фабрика.
- «Хочу простоту и тестируемость» → функции-фабрики и dependency injection.
Mermaid диаграмма принятия решения:
flowchart TD
A[Есть объекты с разными типами?] -->|Да| B[Нужен централизованный создатель?]
A -->|Нет| C[Есть цель скрыть состояние?]
B -->|Да| D[Фабрика]
B -->|Нет| E[Простая конструкция без фабрики]
C -->|Да| F[Модуль/ES6-модуль]
C -->|Нет| EЧеклист по внедрению (роль-based)
Разработчик:
- описать публичный API модуля;
- выделить приватные функции и состояние;
- написать unit-тесты на публичные методы;
- документировать контракты фабрики (входные параметры, возвращаемые свойства).
Технический лидер:
- проверить согласованность подхода в проекте (модули vs ES-модули);
- ограничить размер фабрики (не более X зон ответственности — эвристика: «одна фабрика — один набор типов»);
- утвердить стратегию миграции к ES6, если нужно.
Ревьювер кода:
- убедиться, что приватное состояние недоступно извне;
- проверить отсутствие побочных эффектов в конструкторе фабрики;
- убедиться, что публичный API минимален и предсказуем.
Критерии приёмки
- Инкапсуляция: приватные переменные недоступны извне (проверить через unit-тесты и попытки доступа).
- Поведение фабрики: при разных входах создаются корректные типы объектов.
- Контракты: публичные методы возвращают ожидаемые значения и выбрасывают предсказуемые ошибки при неправильных входных данных.
- Отсутствие утечек памяти: модули не удерживают излишние ссылки, тестировать профилировщиком при нагрузке.
- Документация: README или комментарии описывают API и примеры использования.
Примеры тестов (идеи):
- unit: создать объект через фабрику и проверить поля name/type/priority;
- интеграция: вызвать UIController.callChangeComponent() и убедиться, что текст в h1 изменился;
- контракт: попытка вызвать несуществующий публичный метод должна давать понятную ошибку.
Совместимость и миграция
- ES5 IIFE подойдёт для старых проектов и быстрого инкапсулирования без сборщика;
- ES6-модули рекомендуются для современных приложений с bundler-ами (Webpack, Rollup) и для серверного Node.js с поддержкой ES-модулей;
- при миграции от IIFE к ES6-модулям:
- экспортируйте только публичный API;
- проверьте точки импорта и удалите глобальные зависимости;
- используйте named exports для тестируемости.
Пример замены IIFE на ES6-модуль (псевдо):
// uiController.js
let component = 'Replacement Text';
function changeComponent() {
const element = document.querySelector('h1');
element.textContent = component;
}
export function callChangeComponent() { changeComponent(); }Частые ошибки и примеры отказов
- Открывать слишком много деталей в публичном API. Результат — сложность сопровождения.
- Дублировать логику создания объектов в разных местах вместо использования фабрики.
- Использовать фабрику как хранилище состояния (фабрика должна только создавать — состояние храните отдельно).
- Забивать модуль побочными эффектами при инициализации (например, манипуляциями DOM сразу при объявлении) — инициируйте модули явным вызовом.
Edge-case: если вам нужны разные реализации одного интерфейса в рантайме (плагины), лучше применить паттерн стратегия или абстрактную фабрику.
Мини-методология внедрения паттернов
- Опишите проблему: зачем нужна инкапсуляция или централизованное создание.
- Выберите паттерн: модуль для инкапсуляции, фабрика для создания объектов.
- Разбейте на шаги: реализовать базовую версию → покрыть тестами → профилировать → документировать.
- Реализуйте минимально жизнеспособную версию (MVP).
- Итерируйте: добавьте улучшения (регистры типов, DI, кеширование).
Короткий глоссарий (1‑строчка)
- IIFE — немедленно вызываемая функция, использующаяся для создания локального скоупа.
- ES6-модули — стандартный способ экспорта/импорта кода в современном JS.
- Фабрика — функция/объект, создающий и возвращающий экземпляры нужных типов.
- Инкапсуляция — сокрытие внутренней реализации за публичным интерфейсом.
Практические сниппеты и чеклист — шпаргалка
Шпаргалка по модулю (быстро):
- Сделать IIFE или ES6-модуль.
- Вернуть минимальный объект API.
- Скрывaть состояние внутри.
Шпаргалка по фабрике (быстро):
- Использовать map вместо цепочки if/else для большого числа типов.
- Возвращать объекты с обычными методами, а не с побочными эффектами.
- Добавить тесты на каждый тип.
Пример карты типов:
const typeMap = {
urgent: (name) => ({ name, priority: 'as soon as possible' }),
trivial: (name) => ({ name, priority: 'when you can' })
};
function createTask(name, type) {
const creator = typeMap[type];
if (!creator) throw new Error('Unknown type');
const task = creator(name);
task.type = type;
task.define = function() { console.log(`${this.name} (${this.type}): ${this.priority}`)};
return task;
}Резюме
Выучив модульный паттерн и паттерн фабрики, вы получите основные инструменты для организации масштабируемого и сопровождаемого JavaScript-кода. Модуль хорош для инкапсуляции и создания стабильного API; фабрика — для централизованного создания схожих объектов. В современных проектах предпочтительнее комбинировать паттерны с ES6-модулями, функциями-фабриками и dependency injection.
Image Credit: Alltechbuzz/ Pixabay
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone