JavaScript Design Patterns
Learn common JavaScript design patterns: Module, Singleton, Factory, Observer, and more. Essential for writing maintainable, scalable code.
Topics Covered:
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
// 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()); // 2Module 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
// 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
// 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
// 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.