Skip to main content

Що таке патерн Module?

Module pattern (патерн Модуль) - це патерн проектування, який огортає код в IIFE для створення приватної області видимості і повертає об'єкт як публічний API.

Теорія

TL;DR

  • Код огортається в IIFE (функцію, що одразу викликається) - це дає приватну область видимості
  • Змінні всередині ніколи не витікають назовні
  • Повернений об'єкт - це публічний API; доступно тільки те, що ти повертаєш
  • До ES6 модулів цей патерн був стандартним способом уникати забруднення глобального простору
  • Сьогодні зустрічається в legacy-коді та там, де потрібен singleton з приватним станом

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

javascript
const Counter = (function() { // count приватний - ззовні недоступний let count = 0; return { increment() { count++; }, decrement() { count--; }, getCount() { return count; } }; })(); Counter.increment(); Counter.increment(); console.log(Counter.getCount()); // 2 console.log(Counter.count); // undefined - справді приватне

IIFE виконується один раз, створює замикання (closure) над count і зникає. Залишається тільки повернений об'єкт. count живе в пам'яті, але дістатись до нього ззовні неможливо.

Як це працює

Два механізми: IIFE і замикання.

IIFE виконується одразу і після цього посилання на функцію зникає. Але внутрішні функції increment, decrement і getCount досі тримають посилання на область видимості, де живе count. Це і є замикання. Поки ці функції існують, count залишається в пам'яті і доступний тільки через них.

Це не симуляція приватності. Counter.count повертає undefined, бо на об'єкті немає властивості з таким ім'ям. Справжня змінна - локальна, в безіменній області видимості, до якої більше ніхто не має доступу.

Revealing Module Pattern

Є варіація, яку варто знати - Revealing Module Pattern. Замість того, щоб оголошувати методи прямо в об'єкті, що повертається, все спочатку оголошується як приватне, а потім вибірково відкривається.

javascript
const UserStore = (function() { let users = []; function add(user) { users.push(user); } function getAll() { return [...users]; // копія, не посилання } function count() { return users.length; } // Відкриваємо тільки те, що потрібно return { add, getAll, count }; })(); UserStore.add({ id: 1, name: 'Alice' }); console.log(UserStore.getAll()); // [{ id: 1, name: 'Alice' }]

Вся логіка оголошується однаково. З одного погляду на return видно, що публічне, а все вище - приватне.

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

  • Потрібен singleton з приватним внутрішнім станом
  • Середовище без ES6 або скрипт без бандлера
  • Треба обгорнути SDK або бібліотеку і відкрити лише чистий інтерфейс
  • Самодостатній скрипт, який не повинен торкатись глобального простору

Module pattern проти ES6 модулів

ES6 модулі (import/export) вирішують ту саму проблему на рівні мови. Файл з ES6 модулем має власну область видимості за замовчуванням - IIFE не потрібен. Все, що не експортується, автоматично приватне.

javascript
// ES6 модуль - userStore.js let users = []; export function add(user) { users.push(user); } export function getAll() { return [...users]; }

Module pattern залишається доречним, якщо все в одному файлі без бандлера, бібліотека підключається через <script>, або потрібна фабрика незалежних екземплярів.

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

Повернення посилання замість копії

Більшість багів, які я бачив із цим патерном, починались з того, що хтось повертав пряме посилання на внутрішній масив або об'єкт.

javascript
const Store = (function() { let items = []; return { getItems() { return items; }, // неправильно - повертає посилання getItemsSafe() { return [...items]; } // правильно - копія }; })(); const ref = Store.getItems(); ref.push('injected'); // приватний масив тепер змінено ззовні

Повертай копію. Завжди.

Пастка Revealing Module Pattern

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

Використання патерну там, де є ES6 модулі

В будь-якому сучасному проекті з бандлером - використовуй import/export. Module pattern - це обхідне рішення для відсутніх мовних можливостей. В React або Node.js проекті він додає складність без користі.

Де зустрічається в реальному коді

  • jQuery огортав всю бібліотеку в один $ простір саме цим патерном
  • Старі браузерні SDK (Google Analytics, legacy Stripe.js) досі використовують його, щоб не засмічувати глобальний об'єкт
  • Node-пакети до CommonJS використовували варіації цього підходу
  • На співбесідах він зустрічається регулярно, бо тестує знання замикань напряму

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

Q: Що таке IIFE і навіщо він потрібен у Module pattern?
A: IIFE - це функція, яка викликається одразу після оголошення. Module pattern використовує її, щоб створити тимчасову приватну область видимості. Після виконання IIFE зовнішня область зникає, але замикання, які вона створила, досі тримають посилання на внутрішні змінні.

Q: Що робити, якщо двом модулям потрібен спільний стан?
A: За задумом - вони не можуть його мати. Кожен IIFE отримує власну область видимості. Якщо потрібен спільний стан між модулями, його треба відкрити через публічні методи або винести в третій модуль, який обидва використовують.

Q: Чим Module pattern відрізняється від класу з приватними полями (#field)?
A: Клас створює ланцюжок прототипів і підтримує кілька екземплярів через new. Module pattern в IIFE-формі зазвичай дає singleton. Приватні поля ES2022 (#) - це класовий аналог того, що Module pattern досягає через замикання, але з кращою підтримкою в інструментах і чіткішим синтаксисом.

Q: Як створити кілька незалежних екземплярів?
A: Прибрати IIFE і зробити фабричну функцію. Кожен виклик створює нове замикання з власним станом.

javascript
function makeCounter() { let count = 0; return { increment() { count++; }, getCount() { return count; } }; } const c1 = makeCounter(); const c2 = makeCounter(); // незалежний екземпляр, окремий count

Q: Що інтерв'юер насправді хоче почути про Module pattern?
A: Що ти розумієш: патерн побудований на замиканнях, "приватне" тут - це не ключове слово мови, а просто недосяжна область видимості, і ти знаєш, коли краще взяти ES6 модулі.

Приклади

Базовий лічильник

javascript
const Counter = (function() { let count = 0; return { increment() { count++; }, reset() { count = 0; }, getCount() { return count; } }; })(); Counter.increment(); Counter.increment(); console.log(Counter.getCount()); // 2 Counter.count = 999; // не має ефекту на внутрішню змінну console.log(Counter.getCount()); // все одно 2

Counter.count = 999 просто створює нову властивість на поверненому об'єкті. Внутрішня змінна count не змінюється - вона живе в іншій області видимості.

API-клієнт з приватним конфігом

javascript
const ApiClient = (function() { const BASE_URL = 'https://api.example.com'; let authToken = null; function buildHeaders() { return { 'Content-Type': 'application/json', ...(authToken && { Authorization: `Bearer ${authToken}` }) }; } return { setToken(token) { authToken = token; }, async get(path) { const response = await fetch(`${BASE_URL}${path}`, { headers: buildHeaders() }); return response.json(); }, async post(path, body) { const response = await fetch(`${BASE_URL}${path}`, { method: 'POST', headers: buildHeaders(), body: JSON.stringify(body) }); return response.json(); } }; })(); ApiClient.setToken('my-secret-token'); ApiClient.get('/users'); // BASE_URL і authToken недоступні ззовні

BASE_URL і authToken закриті всередині замикання. Зовнішній код може викликати setToken, щоб оновити токен, але прочитати його напряму - ні.

Фабрика для кількох екземплярів

javascript
function createFormValidator(rules) { const errors = {}; let submitted = false; function validate(data) { Object.keys(rules).forEach(field => { if (rules[field].required && !data[field]) { errors[field] = `${field} is required`; } }); return Object.keys(errors).length === 0; } return { validate, submit(data) { if (validate(data)) { submitted = true; return true; } return false; }, getErrors() { return { ...errors }; }, isSubmitted() { return submitted; } }; } const loginValidator = createFormValidator({ email: { required: true }, password: { required: true } }); const signupValidator = createFormValidator({ email: { required: true }, username: { required: true }, password: { required: true } }); // Кожен валідатор має повністю незалежний стан loginValidator.submit({ email: 'a@b.com' }); // fails: password missing console.log(signupValidator.isSubmitted()); // false - не зачеплений

Кожен виклик createFormValidator створює нове замикання з власними errors і submitted. Два валідатори не мають жодного спільного стану.

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

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

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

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