Design Patterns and Architecture
70 minDesign patterns are battle-tested solutions to common software design problems. In JavaScript, they help structure complex applications and manage state effectively.
**Creational Patterns**: Singleton (one instance), Factory (create objects without specifying exact class).
**Structural Patterns**: Module (encapsulation), Adapter (incompatible interfaces work together), Decorator (add behavior dynamically).
**Behavioral Patterns**: Observer (pub/sub event handling), Strategy (interchangeable algorithms), Command (encapsulate request as object).
Key Concepts
- Singleton & Factory Patterns.
- Observer/Pub-Sub Pattern.
- Module Pattern (IIFE/ESM).
- MVC / MVVM Architecture.
- SOLID Principles.
Learning Objectives
Master
- Implementing Singleton and Factory
- Building an Event Emitter (Observer)
- Refactoring spaghetti code with patterns
- Applying SOLID principles in JS
Develop
- Architectural thinking
- Code maintainability and scalability
- Recognizing patterns in libraries
Tips
- Don't force a pattern if a simple function will do (YAGNI - You Ain't Gonna Need It).
- The Observer pattern is the heart of most UI frameworks (state changes -> UI updates).
- Use the Module pattern (or ES Modules) to avoid polluting the global namespace.
- Learn the patterns used by your framework (e.g., React uses Composition and HOCs).
Common Pitfalls
- Over-engineering: Implementing complex patterns for a simple To-Do app.
- Singleton abuse: Using Singletons as global state buckets (makes testing hard).
- Not adapting patterns to JavaScript's strengths (e.g., trying to force strict Java-like OOP).
- Ignoring the 'S' in SOLID (Single Responsibility Principle) - giant 'God Objects'.
Summary
- Patterns solve common problems.
- They provide a shared vocabulary.
- Architecture scales your app.
- Use them wisely, not blindly.
Exercise
Implement a comprehensive design pattern demonstration that showcases multiple patterns working together in a real-world scenario.
// Design Patterns Implementation
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(listener => listener(data));
}
}
off(event, listener) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(l => l !== listener);
}
}
}
// Singleton Pattern
class Logger extends EventEmitter {
constructor() {
super();
if (Logger.instance) {
return Logger.instance;
}
Logger.instance = this;
this.logs = [];
}
log(message, level = 'info') {
const logEntry = {
message,
level,
timestamp: new Date().toISOString()
};
this.logs.push(logEntry);
this.emit('log', logEntry);
console.log(`[${level.toUpperCase()}] ${message}`);
}
getLogs() {
return [...this.logs];
}
clearLogs() {
this.logs = [];
this.emit('logsCleared');
}
}
// Factory Pattern
class UserFactory {
static createUser(type, data) {
switch (type) {
case 'admin':
return new AdminUser(data);
case 'regular':
return new RegularUser(data);
case 'guest':
return new GuestUser(data);
default:
throw new Error(`Unknown user type: ${type}`);
}
}
}
// Strategy Pattern
class ValidationStrategy {
static strategies = {
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
password: (value) => value.length >= 8 && /[A-Z]/.test(value) && /[a-z]/.test(value) && /\d/.test(value),
phone: (value) => /^\+?[\d\s\-\(\)]{10,}$/.test(value)
};
static validate(type, value) {
const strategy = this.strategies[type];
if (!strategy) {
throw new Error(`Unknown validation type: ${type}`);
}
return strategy(value);
}
}
// Observer Pattern Implementation
class UserManager extends EventEmitter {
constructor() {
super();
this.users = new Map();
this.logger = new Logger();
// Subscribe to logger events
this.logger.on('log', (logEntry) => {
if (logEntry.level === 'error') {
this.emit('userError', logEntry);
}
});
}
addUser(user) {
if (this.users.has(user.id)) {
this.logger.log(`User ${user.id} already exists`, 'error');
return false;
}
this.users.set(user.id, user);
this.logger.log(`User ${user.id} added successfully`);
this.emit('userAdded', user);
return true;
}
removeUser(userId) {
if (!this.users.has(userId)) {
this.logger.log(`User ${userId} not found`, 'error');
return false;
}
const user = this.users.get(userId);
this.users.delete(userId);
this.logger.log(`User ${userId} removed successfully`);
this.emit('userRemoved', userId);
return true;
}
getUser(userId) {
return this.users.get(userId);
}
getAllUsers() {
return Array.from(this.users.values());
}
}
// User Classes
class BaseUser {
constructor(data) {
this.id = data.id;
this.name = data.name;
this.email = data.email;
this.createdAt = new Date();
}
validate() {
const errors = [];
if (!ValidationStrategy.validate('email', this.email)) {
errors.push('Invalid email format');
}
return errors;
}
}
class AdminUser extends BaseUser {
constructor(data) {
super(data);
this.role = 'admin';
this.permissions = data.permissions || ['read', 'write', 'delete'];
}
hasPermission(permission) {
return this.permissions.includes(permission);
}
}
class RegularUser extends BaseUser {
constructor(data) {
super(data);
this.role = 'regular';
this.permissions = data.permissions || ['read', 'write'];
}
}
class GuestUser extends BaseUser {
constructor(data) {
super(data);
this.role = 'guest';
this.permissions = data.permissions || ['read'];
}
}
// Command Pattern
class Command {
constructor(execute, undo) {
this.execute = execute;
this.undo = undo;
}
}
class CommandManager {
constructor() {
this.commands = [];
this.currentIndex = -1;
}
execute(command) {
command.execute();
this.commands.push(command);
this.currentIndex++;
}
undo() {
if (this.currentIndex >= 0) {
const command = this.commands[this.currentIndex];
command.undo();
this.currentIndex--;
}
}
redo() {
if (this.currentIndex < this.commands.length - 1) {
this.currentIndex++;
const command = this.commands[this.currentIndex];
command.execute();
}
}
}
// Usage Example
function demonstratePatterns() {
console.log('=== Design Patterns Demo ===\n');
// Create user manager
const userManager = new UserManager();
const commandManager = new CommandManager();
// Subscribe to events
userManager.on('userAdded', (user) => {
console.log(`🎉 User added: ${user.name} (${user.role})`);
});
userManager.on('userRemoved', (user) => {
console.log(`👋 User removed: ${user.name} (${user.role})`);
});
userManager.on('userError', (error) => {
console.log(`❌ User error: ${error.message}`);
});
// Create users using factory
const adminData = { id: '1', name: 'Admin User', email: 'admin@example.com' };
const regularData = { id: '2', name: 'Regular User', email: 'user@example.com' };
const guestData = { id: '3', name: 'Guest User', email: 'guest@example.com' };
const admin = UserFactory.createUser('admin', adminData);
const regular = UserFactory.createUser('regular', regularData);
const guest = UserFactory.createUser('guest', guestData);
// Add users with command pattern
const addUserCommand = new Command(
() => userManager.addUser(admin),
() => userManager.removeUser(admin.id)
);
const addRegularCommand = new Command(
() => userManager.addUser(regular),
() => userManager.removeUser(regular.id)
);
commandManager.execute(addUserCommand);
commandManager.execute(addRegularCommand);
// Test validation
console.log('\n=== Validation Test ===');
const invalidUser = UserFactory.createUser('regular', {
id: '4',
name: 'Invalid User',
email: 'invalid-email'
});
const validationErrors = invalidUser.validate();
if (validationErrors.length > 0) {
console.log('Validation errors:', validationErrors);
}
// Test permissions
console.log('\n=== Permission Test ===');
console.log(`Admin can delete: ${admin.hasPermission('delete')}`);
console.log(`Regular user can delete: ${regular.hasPermission('delete')}`);
console.log(`Guest can write: ${guest.hasPermission('write')}`);
// Test undo/redo
console.log('\n=== Undo/Redo Test ===');
console.log('Users before undo:', userManager.getAllUsers().length);
commandManager.undo();
console.log('Users after undo:', userManager.getAllUsers().length);
commandManager.redo();
console.log('Users after redo:', userManager.getAllUsers().length);
// Show logs
console.log('\n=== Logger Test ===');
const logs = userManager.logger.getLogs();
console.log('Total log entries:', logs.length);
console.log('Recent logs:', logs.slice(-3));
}
// Run demonstration
if (typeof window !== 'undefined') {
// Browser environment
window.demonstratePatterns = demonstratePatterns;
} else {
// Node.js environment
demonstratePatterns();
}