Common JavaScript Design Patterns

Design patterns are reusable solutions to common programming problems. Understanding these patterns helps write more maintainable and scalable JavaScript applications.

Singleton Pattern

// Classic Singleton
const Database = (function() {
  let instance;
  
  function createInstance() {
    return {
      data: [],
      add(item) { this.data.push(item); },
      remove(item) { /* ... */ }
    };
  }
  
  return {
    getInstance() {
      if (!instance) instance = createInstance();
      return instance;
    }
  };
})();

// ES6 Singleton
class Settings {
  static instance;
  constructor() {
    if (Settings.instance) return Settings.instance;
    Settings.instance = this;
  }
}

Observer Pattern

class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(event, callback) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(callback);
  }

  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
}

// Usage
const emitter = new EventEmitter();
emitter.on('userUpdated', user => console.log(user));
emitter.emit('userUpdated', { name: 'John' });

Factory Pattern

// Simple Factory
class UserFactory {
  createUser(type) {
    switch(type) {
      case 'admin':
        return new AdminUser();
      case 'regular':
        return new RegularUser();
      default:
        throw new Error('Invalid user type');
    }
  }
}

// Abstract Factory
class UIFactory {
  createButton() { /* ... */ }
  createInput() { /* ... */ }
}

class DarkThemeFactory extends UIFactory {
  createButton() { return new DarkButton(); }
  createInput() { return new DarkInput(); }
}

Decorator Pattern

// Class Decorator
function readonly(target, key, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

class Example {
  @readonly
  pi() { return 3.14; }
}

// Function Decorators
function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`Calling ${name} with `, args);
    return original.apply(this, args);
  };
  return descriptor;
}

class Math {
  @log
  add(a, b) { return a + b; }
}

Module Pattern

// Revealing Module Pattern
const ShoppingCart = (function() {
  // Private variables
  let items = [];
  
  // Private methods
  function calculateTotal() {
    return items.reduce((total, item) => total + item.price, 0);
  }
  
  // Public API
  return {
    addItem(item) {
      items.push(item);
    },
    getTotal() {
      return calculateTotal();
    }
  };
})();

Common Interview Follow-up Questions

  1. When would you use a Singleton vs Module pattern?
  2. How do you implement the Observer pattern in React?
  3. What are the trade-offs of using the Factory pattern?
  4. How do decorators improve code reusability?

Best Practices

  • Choose patterns based on specific needs
  • Don't over-engineer solutions
  • Consider maintainability and testability
  • Document pattern usage in your codebase
  • Be consistent with pattern implementation