Intermediate80 min read

JavaScript Design Patterns

Learn common JavaScript design patterns: Module, Singleton, Factory, Observer, and more. Essential for writing maintainable, scalable code.

Topics Covered:

Module PatternSingletonFactoryObserverStrategyDecorator

Prerequisites:

  • JavaScript Closures and Scope Deep Dive

Video Tutorial

Overview

Design patterns are reusable solutions to common programming problems. Understanding design patterns helps you write more maintainable, scalable, and testable code. This tutorial covers essential JavaScript design patterns including Module, Singleton, Factory, Observer, Strategy, and Decorator patterns. These patterns are used in React libraries and can help you structure your React applications better.

Module Pattern

The Module pattern provides a way to create private and public members, encapsulating functionality. Module Pattern: • Encapsulates code • Private and public members • Avoids global namespace pollution • Can use IIFE or ES6 modules Benefits: • Code organization • Privacy • Reusability • Maintainability

Code Example:
// Module pattern with IIFE
const MyModule = (function() {
  // Private variables
  let privateVar = 0;
  
  // Private function
  function privateFunction() {
    return privateVar;
  }
  
  // Public API
  return {
    // Public method
    publicMethod: function() {
      return privateFunction();
    },
    
    // Public method that modifies private state
    setValue: function(val) {
      privateVar = val;
    },
    
    // Public property
    publicProperty: "I'm public"
  };
})();

MyModule.setValue(10);
console.log(MyModule.publicMethod()); // 10
console.log(MyModule.publicProperty); // "I'm public"
// console.log(MyModule.privateVar); // undefined (private)

// Module pattern with ES6 modules
// mathUtils.js
let privateCounter = 0;

export function increment() {
  privateCounter++;
  return privateCounter;
}

export function decrement() {
  privateCounter--;
  return privateCounter;
}

export function getCount() {
  return privateCounter;
}

// main.js
import { increment, decrement, getCount } from './mathUtils';

increment();
increment();
console.log(getCount()); // 2

Module pattern encapsulates code with private and public members. Use IIFE for traditional modules or ES6 modules for modern JavaScript. Keeps code organized and prevents global namespace pollution.

Singleton Pattern

Singleton ensures a class has only one instance and provides global access to it. Singleton Pattern: • Only one instance exists • Global access point • Lazy initialization • Useful for shared resources Use Cases: • Configuration objects • Database connections • Logging services • Cache managers

Code Example:
// Singleton with function
const Singleton = (function() {
  let instance;
  
  function createInstance() {
    return {
      name: "Singleton Instance",
      getData: function() {
        return "Some data";
      }
    };
  }
  
  return {
    getInstance: function() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true (same instance)

// Singleton with class
class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }
    
    this.connection = "Connected";
    Database.instance = this;
    return this;
  }
  
  query(sql) {
    return `Executing: ${sql}`;
  }
}

const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true

// Modern ES6 singleton
class Config {
  static instance = null;
  
  constructor() {
    if (Config.instance) {
      return Config.instance;
    }
    
    this.settings = {};
    Config.instance = this;
  }
  
  set(key, value) {
    this.settings[key] = value;
  }
  
  get(key) {
    return this.settings[key];
  }
}

const config1 = new Config();
const config2 = new Config();
config1.set("theme", "dark");
console.log(config2.get("theme")); // "dark" (same instance)

Singleton ensures only one instance exists. Useful for shared resources like configuration, database connections, or logging services. Be careful - can make testing harder.

Factory Pattern

Factory pattern creates objects without specifying the exact class. It provides a way to create objects based on a condition or parameter. Factory Pattern: • Creates objects based on input • Hides object creation logic • Centralizes object creation • Flexible and extensible Use Cases: • Creating different object types • Complex object initialization • Dynamic object creation • Plugin systems

Code Example:
// Simple factory function
function createUser(type, name) {
  switch(type) {
    case "admin":
      return {
        name,
        role: "admin",
        permissions: ["read", "write", "delete"]
      };
    case "user":
      return {
        name,
        role: "user",
        permissions: ["read"]
      };
    default:
      throw new Error("Unknown user type");
  }
}

const admin = createUser("admin", "Alice");
const user = createUser("user", "Bob");

// Factory with classes
class Car {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }
}

class Truck {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }
}

class VehicleFactory {
  createVehicle(type, make, model) {
    switch(type) {
      case "car":
        return new Car(make, model);
      case "truck":
        return new Truck(make, model);
      default:
        throw new Error("Unknown vehicle type");
    }
  }
}

const factory = new VehicleFactory();
const car = factory.createVehicle("car", "Toyota", "Camry");
const truck = factory.createVehicle("truck", "Ford", "F-150");

// Factory in React (preview)
function createComponent(type, props) {
  const components = {
    button: () => <button {...props} />,
    input: () => <input {...props} />,
    div: () => <div {...props} />
  };
  
  return components[type]?.() || null;
}

Factory pattern centralizes object creation. Creates objects based on input without exposing creation logic. Useful for creating different types of objects dynamically.

Observer Pattern

Observer pattern defines a one-to-many dependency between objects. When one object changes state, all dependents are notified. Observer Pattern: • Subject maintains list of observers • Observers subscribe/unsubscribe • Subject notifies observers of changes • Loose coupling between subject and observers Use Cases: • Event systems • Model-View updates • React state management • Pub/Sub systems

Code Example:
// Observer pattern implementation
class Subject {
  constructor() {
    this.observers = [];
  }
  
  subscribe(observer) {
    this.observers.push(observer);
  }
  
  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }
  
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }
  
  update(data) {
    console.log(`${this.name} received: ${data}`);
  }
}

// Usage
const subject = new Subject();
const observer1 = new Observer("Observer 1");
const observer2 = new Observer("Observer 2");

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify("Hello!"); 
// Observer 1 received: Hello!
// Observer 2 received: Hello!

subject.unsubscribe(observer1);
subject.notify("Goodbye!");
// Observer 2 received: Goodbye!

// Observer pattern in React (preview)
// React's useState is similar to observer pattern
function useObserver(initialValue) {
  const [value, setValue] = useState(initialValue);
  const observers = useRef([]);
  
  const subscribe = useCallback((callback) => {
    observers.current.push(callback);
    return () => {
      observers.current = observers.current.filter(cb => cb !== callback);
    };
  }, []);
  
  const update = useCallback((newValue) => {
    setValue(newValue);
    observers.current.forEach(callback => callback(newValue));
  }, []);
  
  return { value, update, subscribe };
}

Observer pattern enables one-to-many communication. Subject notifies observers of changes. Similar to React's state management and event systems. Enables loose coupling.

Conclusion

Design patterns provide proven solutions to common problems. You've learned Module, Singleton, Factory, and Observer patterns. These patterns help you write more maintainable, scalable code. Understanding patterns helps you recognize them in libraries and frameworks, and apply them in your own code. Practice implementing these patterns and look for opportunities to use them in your React applications.