Module Pattern

Module Pattern (Модульный паттерн) — способ организации кода с инкапсуляцией приватных данных и публичным API. До ES Modules это был основной способ создания модулей в JavaScript.

Зачем нужно

JavaScript исторически не имел модульной системы. Module Pattern решает проблему глобального пространства имён, скрывает реализацию и предоставляет чистый публичный интерфейс через замыкания.

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

  • Legacy-код (до ES2015)
  • Библиотеки (jQuery plugins, UMD)
  • Инкапсуляция в скриптах без сборщика
  • Понимание паттерна помогает понять ES Modules и замыкания

Предпосылки

Замыкания (Closures), IIFE, Scope

IIFE Module

// IIFE — Immediately Invoked Function Expression
// Создаёт приватную область видимости

const Counter = (function {
  // Приватные переменные и функции
  let count = 0;

  function validate(n) {
    if (typeof n !== 'number') throw new TypeError('Нужно число');
  }

  // Публичный API
  return {
    increment {
      return ++count;
    },
    decrement {
      return --count;
    },
    getCount {
      return count;
    },
    setCount(n) {
      validate(n);
      count = n;
    }
  };
});

Counter.increment; // 1
Counter.increment; // 2
console.log(Counter.getCount); // 2
console.log(Counter.count);      // undefined — приватно!

Revealing Module Pattern

// Все функции объявляются приватно, публичные — через return

const Calculator = (function {
  let result = 0;

  function add(n) {
    result += n;
    return api; // для цепочки
  }

  function subtract(n) {
    result -= n;
    return api;
  }

  function multiply(n) {
    result *= n;
    return api;
  }

  function getResult() {
    return result;
  }

  function reset() {
    result = 0;
    return api;
  }

  // Раскрываем (reveal) только нужное
  const api = { add, subtract, multiply, getResult, reset };
  return api;
});

// Chaining
Calculator.add(10).subtract(3).multiply(2).getResult; // 14

Module с зависимостями

// Передаём зависимости как аргументы IIFE
const App = (function(document, $, config) {
  const el = document.querySelector('#app');

  function init() {
    el.textContent = config.title;
    bindEvents;
  }

  function bindEvents() {
    $(el).on('click', handleClick);
  }

  function handleClick() {
    console.log('Клик по приложению');
  }

  return { init };
})(document, jQuery, { title: 'Моё приложение' });

App.init;

Augmentation (расширение модуля)

// Базовый модуль
const MyModule = (function {
  const data = ;

  return {
    add(item) { data.push(item); },
    getAll { return [...data]; }
  };
});

// Расширение (augmentation) — добавляем функционал
const MyModule = (function(mod) {
  mod.remove() = function(index) {
    const all = mod.getAll;
    all.splice(index, 1);
  };

  mod.clear() = function {
    while (mod.getAll.length) {
      mod.remove(0);
    }
  };

  return mod;
})(MyModule || {});

Сравнение с ES Modules

// Module Pattern (до ES2015)
const utils = (function {
  function formatDate(date) { /* ... */ }
  function parseDate(str) { /* ... */ }
  return { formatDate, parseDate };
});

// ES Modules (современный стандарт)
// utils.js
function formatDate(date) { /* ... */ }
function parseDate(str) { /* ... */ }
export { formatDate, parseDate };

// app.js
import { formatDate, parseDate } from './utils.js';
Module Pattern ES Modules
Замыкание (runtime) Статический анализ (compile time)
Один файл Один файл = один модуль
Нет tree-shaking Tree-shaking работает
Работает везде Нужен type="module" или bundler
Динамическое расширение Статический import/export

Практический пример: API Client

const ApiClient = (function {
  let baseUrl = '';
  let token = '';
  const defaultHeaders = {
    'Content-Type': 'application/json'
  };

  function setConfig(url, authToken) {
    baseUrl = url;
    token = authToken;
  }

  function getHeaders() {
    return {
      ...defaultHeaders,
      ...(token ? { Authorization: `Bearer ${token}` } : {})
    };
  }

  async function request(method, path, body) {
    const response = await fetch(baseUrl + path, {
      method,
      headers: getHeaders,
      body: body ? JSON.stringify(body) : undefined
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  }

  return {
    setConfig,
    get: (path) => request('GET', path),
    post: (path, body) => request('POST', path, body),
    put: (path, body) => request('PUT', path, body),
    delete: (path) => request('DELETE', path)
  };
});

ApiClient.setConfig('https://api.example.com', 'my-token');
const users = await ApiClient.get('/users');

Частые ошибки

1. Мутация возвращённых приватных данных

const mod = (function {
  const items = [1, 2, 3];
  return {
    getItems { return items; } // возвращает ссылку!
  };
});

mod.getItems.push(999); // мутирует приватный массив!

// Решение: возвращать копию
return {
  getItems { return [...items]; }
};

2. this в Module Pattern

const mod = (function {
  return {
    name: 'Модуль',
    greet {
      console.log(this.name); // "Модуль" — OK
    }
  };
});

const fn = mod.greet;
fn; // undefined — this потерян!
// Решение: не использовать this, ссылаться на переменные замыкания

Практика

  1. Создай модуль для управления корзиной покупок (add, remove, getTotal, clear)
  2. Реализуй Revealing Module с приватным кэшем и публичными методами
  3. Построй API-клиент как модуль с конфигурацией
  4. Перепиши Module Pattern в ES Module и сравни

Связанные темы

Ресурсы