Back to Curriculum

Design Patterns and Architecture

📚 Lesson 17 of 20 ⏱️ 70 min

Design Patterns and Architecture

70 min

Design 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();
}

Code Editor

Output