Learning Objectives
- Understand the Module pattern concept
- Create private and public members
- Use IIFE for encapsulation
- Implement revealing module pattern
- Apply module best practices
What is the Module Pattern?
Module pattern uses IIFE (Immediately Invoked Function Expression) and closures to create private scope and expose only what's needed. It was the primary way to achieve encapsulation before ES6 modules.
Basic Implementation
const Counter = (function() {
// Private variable - not accessible outside
let count = 0;
// Private function
function log() {
console.log(`Current count: ${count}`);
}
// Public API - returned object
return {
increment() {
count++;
log();
return this;
},
decrement() {
count--;
log();
return this;
},
getCount() {
return count;
},
reset() {
count = 0;
log();
}
};
})();
// Usage
Counter.increment(); // Current count: 1
Counter.increment(); // Current count: 2
Counter.decrement(); // Current count: 1
console.log(Counter.getCount()); // 1
console.log(Counter.count); // undefined (private!)
// Method chaining
Counter.increment().increment().increment();
Revealing Module Pattern
A cleaner variation that defines all functions first, then reveals them:
const Calculator = (function() {
let result = 0;
function add(x) {
result += x;
return this;
}
function subtract(x) {
result -= x;
return this;
}
function multiply(x) {
result *= x;
return this;
}
function divide(x) {
if (x === 0) {
throw new Error('Cannot divide by zero');
}
result /= x;
return this;
}
function getResult() {
return result;
}
function reset() {
result = 0;
return this;
}
// Reveal only what's needed
return {
add,
subtract,
multiply,
divide,
getResult,
reset
};
})();
// Usage
Calculator
.add(10)
.multiply(2)
.subtract(5)
.divide(3);
console.log(Calculator.getResult()); // 5
Calculator.reset();
Real-World Examples
User Manager Module
const UserManager = (function() {
const users = [];
let nextId = 1;
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function findById(id) {
return users.find(user => user.id === id);
}
function addUser(name, email) {
if (!validateEmail(email)) {
throw new Error('Invalid email');
}
const user = {
id: nextId++,
name,
email,
createdAt: new Date()
};
users.push(user);
return user;
}
function removeUser(id) {
const index = users.findIndex(user => user.id === id);
if (index === -1) {
throw new Error('User not found');
}
return users.splice(index, 1)[0];
}
function getUser(id) {
const user = findById(id);
if (!user) {
throw new Error('User not found');
}
return { ...user }; // Return copy
}
function getAllUsers() {
return users.map(user => ({ ...user }));
}
return {
addUser,
removeUser,
getUser,
getAllUsers
};
})();
// Usage
const user1 = UserManager.addUser('John', 'john@example.com');
const user2 = UserManager.addUser('Jane', 'jane@example.com');
console.log(UserManager.getAllUsers());
// [{ id: 1, name: 'John', ... }, { id: 2, name: 'Jane', ... }]
console.log(UserManager.users); // undefined (private!)
API Client Module
const ApiClient = (function() {
const baseUrl = 'https://api.example.com';
let authToken = null;
async function request(endpoint, options = {}) {
const url = `${baseUrl}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(url, {
...options,
headers
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
function setAuthToken(token) {
authToken = token;
}
function clearAuthToken() {
authToken = null;
}
async function get(endpoint) {
return request(endpoint, { method: 'GET' });
}
async function post(endpoint, data) {
return request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
async function put(endpoint, data) {
return request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
async function del(endpoint) {
return request(endpoint, { method: 'DELETE' });
}
return {
setAuthToken,
clearAuthToken,
get,
post,
put,
delete: del
};
})();
// Usage
ApiClient.setAuthToken('my-token-123');
const users = await ApiClient.get('/users');
const newUser = await ApiClient.post('/users', { name: 'John' });
When to Use Module Pattern
Good use cases:
- Encapsulation and privacy
- Private state management
- Namespace management
- Legacy code (pre-ES6)
- Single instance utilities
Benefits
- Privacy: True private members via closures
- Encapsulation: Hide implementation details
- Clean API: Expose only what's needed
- Namespace protection: Avoid global pollution
- Single instance: One instance per module
Module Pattern vs ES6 Modules
// Module Pattern (old way)
const MyModule = (function() {
const private = 'private';
return {
public() {
return private;
}
};
})();
// ES6 Modules (modern way)
// mymodule.js
const private = 'private';
export function public() {
return private;
}
// app.js
import { public } from './mymodule.js';
Modern Recommendation: Use ES6 modules for new code. Module pattern is still useful for:
- Legacy browser support
- Single-file scripts
- Understanding closures
- Maintaining old code
Best Practices
1. Use revealing module pattern for clarity
// Define all functions first
function method1() {}
function method2() {}
// Then reveal
return { method1, method2 };
2. Return copies of private data
function getUsers() {
return users.map(u => ({ ...u })); // Copy
}
3. Use descriptive names
const UserManager = (function() {
// Clear purpose
})();
Key Takeaways
- Module pattern creates private scope with IIFE
- Uses closures for privacy
- Exposes public API via returned object
- Revealing pattern is cleaner and more readable
- Modern ES6 modules are preferred for new code
- Still useful for understanding encapsulation
- Great for single-instance utilities
- Prevents global namespace pollution