Learning Objectives
- Understand the Facade pattern concept
- Simplify complex APIs
- Hide implementation details
- Create unified interfaces
- Apply facade best practices
What is the Facade Pattern?
A Facade wraps complex subsystems with a simple, unified interface that's easier to use. It hides the complexity and provides a clean API for common tasks.
Basic Example
// Complex subsystem with many classes
class CPU {
freeze() {
console.log('CPU frozen');
}
jump(position) {
console.log(`Jumping to position ${position}`);
}
execute() {
console.log('Executing instructions');
}
}
class Memory {
load(position, data) {
console.log(`Loading ${data} at position ${position}`);
}
}
class HardDrive {
read(sector, size) {
console.log(`Reading ${size} bytes from sector ${sector}`);
return 'boot data';
}
}
// Facade - provides simple interface
class ComputerFacade {
constructor() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
start() {
console.log('Starting computer...');
this.cpu.freeze();
const bootData = this.hardDrive.read(0, 1024);
this.memory.load(0, bootData);
this.cpu.jump(0);
this.cpu.execute();
console.log('Computer started!');
}
}
// Usage - simple and clean!
const computer = new ComputerFacade();
computer.start(); // One simple method call
Real-World Examples
API Facade
class ApiFacade {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.token = null;
}
setAuthToken(token) {
this.token = token;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
const response = await fetch(url, {
...options,
headers
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
async getUser(id) {
return this.request(`/users/${id}`);
}
async createUser(userData) {
return this.request('/users', {
method: 'POST',
body: JSON.stringify(userData)
});
}
async updateUser(id, userData) {
return this.request(`/users/${id}`, {
method: 'PUT',
body: JSON.stringify(userData)
});
}
async deleteUser(id) {
return this.request(`/users/${id}`, {
method: 'DELETE'
});
}
}
// Usage - simple and consistent
const api = new ApiFacade('https://api.example.com');
api.setAuthToken('my-token');
const user = await api.getUser(1);
const newUser = await api.createUser({ name: 'John' });
Browser Storage Facade
class StorageFacade {
set(key, value, expiresIn = null) {
const item = {
value,
timestamp: Date.now(),
expiresIn
};
try {
localStorage.setItem(key, JSON.stringify(item));
return true;
} catch (error) {
console.error('Storage error:', error);
return false;
}
}
get(key) {
try {
const item = JSON.parse(localStorage.getItem(key));
if (!item) return null;
if (item.expiresIn) {
const age = Date.now() - item.timestamp;
if (age > item.expiresIn) {
this.remove(key);
return null;
}
}
return item.value;
} catch (error) {
console.error('Storage error:', error);
return null;
}
}
remove(key) {
localStorage.removeItem(key);
}
clear() {
localStorage.clear();
}
has(key) {
return this.get(key) !== null;
}
}
// Usage - simple interface
const storage = new StorageFacade();
storage.set('user', { name: 'John' }, 3600000); // 1 hour expiry
const user = storage.get('user');
storage.remove('user');
Event Handler Facade
class EventFacade {
constructor(element) {
this.element = element;
this.listeners = new Map();
}
on(event, handler) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(handler);
this.element.addEventListener(event, handler);
return () => this.off(event, handler);
}
off(event, handler) {
const handlers = this.listeners.get(event);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
this.element.removeEventListener(event, handler);
}
once(event, handler) {
const onceHandler = (e) => {
handler(e);
this.off(event, onceHandler);
};
this.on(event, onceHandler);
}
emit(event, data) {
const customEvent = new CustomEvent(event, { detail: data });
this.element.dispatchEvent(customEvent);
}
removeAll(event) {
const handlers = this.listeners.get(event);
if (handlers) {
handlers.forEach(handler => {
this.element.removeEventListener(event, handler);
});
this.listeners.delete(event);
}
}
}
// Usage
const button = document.querySelector('#myButton');
const events = new EventFacade(button);
const unsubscribe = events.on('click', (e) => {
console.log('Clicked!');
});
events.once('click', () => {
console.log('First click only');
});
unsubscribe(); // Easy cleanup
When to Use Facade Pattern
Good use cases:
- Simplify complex APIs or libraries
- Hide implementation details
- Provide unified interface to subsystems
- Reduce dependencies on internal workings
- Improve testability by isolating complexity
- Create convenience methods for common tasks
Benefits
- Simplicity: Easy-to-use interface
- Loose coupling: Clients don't depend on subsystem details
- Easy to use: Common tasks become simple
- Hides complexity: Internal details are hidden
- Flexibility: Can change subsystem without affecting clients
Best Practices
1. Keep facade methods simple
// Good - simple, focused methods
class ApiFacade {
async getUser(id) {
return this.request(`/users/${id}`);
}
}
2. Don't hide everything
// Allow access to subsystems if needed
class Facade {
constructor() {
this.subsystem = new Subsystem();
}
// Simple methods for common tasks
doCommonTask() {
this.subsystem.complexMethod();
}
// But allow direct access if needed
getSubsystem() {
return this.subsystem;
}
}
3. Handle errors gracefully
async request(endpoint) {
try {
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
} catch (error) {
console.error('Request failed:', error);
throw error;
}
}
Key Takeaways
- Facade simplifies complex subsystems
- Provides unified, easy-to-use interface
- Hides implementation details from clients
- Makes code easier to use and test
- Common in library and framework design
- Reduces coupling between client and subsystem
- Don't hide everything - allow access when needed
- Keep facade methods simple and focused