What Are Design Patterns?
Definition: Design patterns are reusable solutions to common problems that occur in software design.
Purpose: They capture best practices learned over time, so you don't have to "reinvent the wheel."
Nature: They are conceptual blueprints, not copy-paste code.
Origin: Popularized by the "Gang of Four" (Gamma, Helm, Johnson, Vlissides) in Design Patterns: Elements of Reusable Object-Oriented Software (1994).
JavaScript relevance: Though JavaScript isn't purely object-oriented, its flexibility (functions, closures, prototypes) allows these patterns to be implemented effectively.
💡 Why Do We Need Design Patterns?
- Common language: They give developers a shared vocabulary (e.g., "Use a Factory here").
- Reusability: Prevents solving the same problems repeatedly.
- Maintainability: Makes code easier to understand, test, and extend.
- Scalability: Provides tried-and-tested ways to structure large applications.
- Communication: Simplifies collaboration and onboarding across teams.
- Consistency: Encourages a standard approach to recurring design challenges.
In short: Patterns make complex systems simpler to reason about.
⚙️ Parts of a Design Pattern
Each pattern is generally described through several key components:
- Pattern Name – A short term for quick reference (e.g., "Singleton," "Observer").
- Intent/Purpose – The goal of the pattern — what problem does it solve?
- Problem – The recurring issue or design challenge being addressed.
- Solution – The general structure or relationships among objects.
- Participants – The classes, functions, or objects involved.
- Collaborations – How these participants interact.
- Consequences (Trade-offs) – The results, benefits, or limitations of applying the pattern.
- Implementation – Code example or recommended approach in JavaScript.
- Known Uses – Real-world or library examples applying this pattern.
🧪 The Patternity Test (Is it really a pattern?)
Definition (short):
A technique qualifies as a design pattern only if it's a named, reusable solution to a recurring problem, with documented trade-offs and proven use across contexts.
Checklist (pass all, or it's not a pattern):
- Recurring problem: Shows up in multiple projects/contexts—not a one-off trick.
- Named solution: Has a stable, recognizable name (e.g., "Observer," not "my cool event thing").
- Context & forces: Clearly states when it applies and what constraints/forces it balances.
- Structure: Describes roles/participants and their collaborations (not just "some code").
- Consequences: Lists pros/cons and trade-offs (what you gain and what you give up).
- Variations: Admits known variants/implementations (e.g., push vs pull Observer).
- Proven: Seen in real systems/libraries; not just hypothetical.
- Reusable & language-agnostic: Idea survives outside one codebase or language quirks.
🏗️ Categories of Design Patterns
Design patterns are grouped into three main families (plus some JS-specific ones):
1. Creational Patterns
Focus: Object creation mechanisms — making the system independent of how objects are created.
Examples: Constructor, Factory, Prototype, Singleton, Builder
Goal: Simplify or control object creation.
2. Structural Patterns
Focus: Object composition — how objects and classes are combined to form larger structures.
Examples: Adapter, Decorator, Facade, Composite, Proxy, Flyweight
Goal: Simplify relationships and improve flexibility.
3. Behavioral Patterns
Focus: Object interaction — how objects communicate and share responsibility.
Examples: Observer, Mediator, Command, Strategy, Iterator, State, Chain of Responsibility, Template Method
Goal: Define clear communication between objects.
4. JavaScript-Specific / Architectural Patterns
Modern JS adds patterns suited to its ecosystem.
Examples: Module, Revealing Module, MVC, MVVM, Pub/Sub
Goal: Organize codebases, manage state, and decouple components.
Constructor Pattern
📘 One-line Definition:
The Constructor Pattern defines a way to create new objects and initialize them with specific properties and methods.
🧩 What It Does
In JavaScript, constructors are functions (or classes) that act as blueprints for creating multiple similar objects.
When used with the new
keyword, they automatically:
- Create a new empty object.
- Bind
this
to that object. - Set the object's internal prototype to the constructor's prototype.
- Return the new object (unless another object is explicitly returned).
🧠 Example
// Constructor function
function Car(model, year, miles) {
this.model = model;
this.year = year;
this.miles = miles;
this.toString = function() {
return `${this.model} has done ${this.miles} miles`;
};
}
// Using the constructor
const civic = new Car("Honda Civic", 2009, 20000);
const mondeo = new Car("Ford Mondeo", 2010, 5000);
console.log(civic.toString()); // "Honda Civic has done 20000 miles"
✅ Modern ES6+ version:
class Car {
constructor(model, year, miles) {
this.model = model;
this.year = year;
this.miles = miles;
}
toString() {
return `${this.model} has done ${this.miles} miles`;
}
}
const civic = new Car("Honda Civic", 2009, 20000);
⚖️ Pros
- Provides a clear structure for creating multiple similar objects.
- Encapsulates initialization logic (ensures objects start consistent).
- Readable and familiar for developers coming from class-based OOP.
- Allows method sharing via the prototype (saves memory).
⚠️ Cons
- If methods are defined inside the constructor, they are re-created per instance, wasting memory.
- Without
new
, calling a constructor behaves unexpectedly (e.g., assigns to window in non-strict mode). - Tight coupling if constructors become too complex or start managing too much logic.
Constructor with Prototype
To avoid recreating methods for each instance, define shared methods on the constructor's prototype:
function Car(model, year, miles) {
this.model = model;
this.year = year;
this.miles = miles;
}
// ✅ Shared method via prototype
Car.prototype.toString = function() {
return `${this.model} has done ${this.miles} miles`;
};
const civic = new Car("Honda Civic", 2009, 20000);
const mondeo = new Car("Ford Mondeo", 2010, 5000);
console.log(civic.toString()); // "Honda Civic has done 20000 miles"
📦 Module Pattern
📘 One-line Definition:
The Module Pattern encapsulates related code (variables and functions) into a single unit, exposing only what's necessary and keeping everything else private using closures.
🧩 What It Does
The Module Pattern helps create private and public parts in your code by leveraging closures and Immediately Invoked Function Expressions (IIFEs).
It allows you to:
- Keep implementation details hidden from the outside world.
- Expose only the methods you want as a public API.
- Organize your codebase into small, maintainable units.
💻 Example — Counter Module
const counter = (function () {
// Private variable
let count = 0;
// Private function
function print(message) {
console.log(`${message} --- Count: ${count}`);
}
// Public API
return {
getCount() {
return count;
},
increment() {
count += 1;
print('After increment');
},
reset() {
print('Before reset');
count = 0;
print('After reset');
}
};
})();
Usage:
counter.increment(); // After increment --- Count: 1
counter.increment(); // After increment --- Count: 2
counter.reset(); // Before reset --- Count: 2
// After reset --- Count: 0
Here:
-
count
andprint()
are private — not accessible from outside. - Only
getCount
,increment
, andreset
are public. - ✅ Encapsulation achieved through closure — the internal state is protected.
⚙️ Modern ES6+ Equivalent
You can achieve the same modular structure using ES Modules:
// counter.js
let count = 0;
function print(message) {
console.log(`${message} --- Count: ${count}`);
}
function getCount() {
return count;
}
function increment() {
count++;
print('After increment');
}
function reset() {
print('Before reset');
count = 0;
print('After reset');
}
export { getCount, increment, reset };
ES Modules naturally create a module scope — anything not exported remains private.
⚖️ Pros
- ✅ Encapsulation: Protects internal data using closure scope.
- ✅ Global safety: Avoids polluting the global namespace.
- ✅ Readable: Clearly distinguishes between public and private parts.
- ✅ Testable API: Only the returned object is accessible for use or testing.
⚠️ Cons
- ❌ Private members are inaccessible for testing or debugging.
- ❌ Can be misused for singletons (if you need multiple instances, prefer factories).
- ❌ IIFE version may feel verbose compared to native ES module syntax.
🔍 Revealing Module Pattern
📘 One-line Definition:
The Revealing Module Pattern improves the classic Module Pattern by defining all variables and functions privately, then exposing only specific parts through an object literal — making the public API clear and intentional.
🧩 What It Does
This pattern is a refinement of the Module Pattern.
Instead of deciding public vs private inside the return object, you define everything privately first, and then reveal only what you want to expose at the end.
Core principles:
- Maintain private scope via closure.
- Clearly define the public interface in one place (the returned object).
- Keep the codebase cleaner and more readable.
💻 Example
const myRevealingModule = (function () {
let privateVar = 'Rohit';
const publicVar = 'Hello World';
function privateFunction() {
console.log(`Name: ${privateVar}`);
}
function publicSetName(name) {
privateVar = name;
}
function publicGetName() {
privateFunction();
}
// Explicitly reveal public pointers to private functions
return {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
})();
Usage:
myRevealingModule.getName(); // "Name: Rohit"
myRevealingModule.setName('Addy');
myRevealingModule.getName(); // "Name: Addy"
console.log(myRevealingModule.greeting); // "Hello World"
✅ Key difference from Module Pattern:
All functions (privateFunction
, publicSetName
, etc.) are defined privately and then "revealed" in the return statement.
⚙️ Why It's Useful
- Keeps the entire logic private first, then selectively reveals only what's needed.
- Ensures the public API is declared in one single location, improving readability.
- Makes refactoring easier — you can rename private functions without changing the exposed API.
⚖️ Pros
- ✅ Clear public API definition — all exports in one place.
- ✅ Improved readability and maintainability compared to the original Module Pattern.
- ✅ Avoids duplication of function definitions (private vs public).
- ✅ Works great for encapsulation and abstraction in small modules or utilities.
⚠️ Cons
- ❌
this
context can become tricky if you rely on it inside private functions. - ❌ Harder to extend or mock private parts for testing.
- ❌ Overusing closures may lead to memory overhead in large apps.
🧩 Singleton Pattern
📘 One-line Definition:
The Singleton Pattern ensures that only one instance of an object exists throughout the application, providing a single global point of access to it.
🧩 What It Does
The Singleton Pattern restricts the instantiation of a class (or object) to just one instance.
If the instance already exists, it simply returns that same object instead of creating a new one.
It's useful for managing shared state, like:
- Application configuration
- Centralized logging
- Caching
- Shared resource access (e.g., database connections)
💻 Example
const mySingleton = (function () {
// Instance stores a reference to the Singleton
let instance;
function init() {
// Private methods and variables
function privateMethod() {
console.log("I am private");
}
const privateVariable = "I'm also private";
const randomNumber = Math.random();
return {
// Public methods and variables
publicMethod() {
console.log("The public can see me!");
},
publicProperty: "I am also public",
getRandomNumber() {
return randomNumber;
}
};
}
return {
// Get the Singleton instance if it exists
// Otherwise create one
getInstance() {
if (!instance) {
instance = init();
}
return instance;
}
};
})();
Usage:
const singleA = mySingleton.getInstance();
const singleB = mySingleton.getInstance();
console.log(singleA.getRandomNumber() === singleB.getRandomNumber());
// true — both share the same instance
✅ Only one instance of mySingleton
is created — subsequent calls return the same object.
⚙️ How It Works
- The outer function (
mySingleton
) acts as a closure that stores the single instance. -
init()
creates and returns the actual instance object. -
getInstance()
checks if the instance already exists — if yes, it returns the existing one; otherwise, it initializes a new one.
⚖️ Pros
- ✅ Guarantees a single shared instance across the app.
- ✅ Useful for shared resources (config, cache, store, connection pool).
- ✅ Provides a global access point to that instance.
- ✅ Easy to implement using closures or ES classes.
⚠️ Cons
- ❌ Can lead to hidden dependencies and tight coupling if used everywhere.
- ❌ Makes testing harder — the global state can persist across tests.
- ❌ May break modular design if overused.
- ❌ Difficult to manage in multi-threaded or concurrent environments (not a JS issue, but conceptually).
👁️ Observer Pattern
📘 One-line Definition:
The Observer Pattern establishes a one-to-many relationship between objects, so when one object's state changes (the Subject), all its dependents (Observers) are automatically notified and updated.
🧩 What It Does
The Observer Pattern is all about communication between objects.
It lets one object (called the Subject) maintain a list of other objects (called Observers) that depend on it.
When the subject's state changes, it notifies all observers — promoting loose coupling and event-driven architecture.
You can think of it as the pattern behind:
- DOM events (
addEventListener
) - Pub/Sub systems
- Reactive programming (RxJS, MobX, Vue reactivity)
💻 Example
1. ObserverList Helper
A simple utility to manage multiple observers.
function ObserverList() {
this.observerList = [];
}
ObserverList.prototype.add = function (obj) {
return this.observerList.push(obj);
};
ObserverList.prototype.count = function () {
return this.observerList.length;
};
ObserverList.prototype.get = function (index) {
if (index > -1 && index < this.observerList.length) {
return this.observerList[index];
}
};
ObserverList.prototype.indexOf = function (obj, startIndex) {
let i = startIndex;
while (i < this.observerList.length) {
if (this.observerList[i] === obj) return i;
i++;
}
return -1;
};
ObserverList.prototype.removeAt = function (index) {
this.observerList.splice(index, 1);
};
2. Subject — The Publisher
function Subject() {
this.observers = new ObserverList();
}
Subject.prototype.addObserver = function (observer) {
this.observers.add(observer);
};
Subject.prototype.removeObserver = function (observer) {
this.observers.removeAt(this.observers.indexOf(observer, 0));
};
Subject.prototype.notify = function (context) {
const observerCount = this.observers.count();
for (let i = 0; i < observerCount; i++) {
this.observers.get(i).update(context);
}
};
3. Observer — The Subscriber
function Observer() {
this.update = function () {
// Default behavior — override when extending
};
}
4. Usage Example
// Create a new subject
const subject = new Subject();
// Create new observers
const observerA = new Observer();
observerA.update = function (value) {
console.log("Observer A received:", value);
};
const observerB = new Observer();
observerB.update = function (value) {
console.log("Observer B received:", value);
};
// Add observers to subject
subject.addObserver(observerA);
subject.addObserver(observerB);
// Notify all observers
subject.notify("Hello Observers!");
Output:
Observer A received: Hello Observers!
Observer B received: Hello Observers!
✅ Both observers are automatically updated whenever the subject calls notify()
.
⚙️ How It Works
- Subject maintains a list of observers.
- Observers register (subscribe) to the subject.
- When the subject's state changes, it notifies all observers.
- Each observer decides how to handle the update independently.
⚖️ Pros
- ✅ Loose coupling: Subjects and observers know nothing about each other's concrete implementation.
- ✅ Scalability: Easily add or remove observers without changing core logic.
- ✅ Reactivity: Forms the basis of reactive/event-driven systems.
- ✅ Extensible: Works well for building Pub/Sub or event emitter systems.
⚠️ Cons
- ❌ Memory leaks possible if observers are not unsubscribed properly.
- ❌ Hard to debug: When many observers react to a single change, it can be tricky to trace updates.
- ❌ Performance overhead if too many observers exist or notifications are frequent.
Observer vs Pub/Sub (At a glance)
Concept | Observer Pattern | Publish/Subscribe Pattern |
---|---|---|
Communication | Subject notifies observers directly | Publisher sends messages through a broker |
Coupling | Tight — Subject holds references to observers | Loose — Publishers and subscribers don't know each other |
Knowledge | Observers must register with a Subject | Publishers/Subscribers only know a topic/channel |
Flow | Subject → Observers | Publisher → Broker → Subscribers |
Use case | UI updates, model–view bindings | Event systems, decoupled app modules, analytics, notifications |
🧭 Core Difference
Observer (direct subscription)
- Who knows whom? Subject holds references to observers
- Coupling: Tighter (subject ↔ observers)
- Flow: Subject calls observer.update(data)
- Naming: No topic names; direct relationship
- Use when: One object's state changes and dependents must react (UI state, DOM events)
Pub/Sub (topic-based broker)
- Who knows whom? Publishers & subscribers know only a topic; a broker routes messages
- Coupling: Looser (no direct references)
- Flow: Publisher → broker → all subscribers of that topic
- Naming: Uses channels/topics (e.g., "mail:new")
- Use when: Many producers/consumers, cross-module events, analytics, logging, integrations
Key differences
- Mediation: Pub/Sub adds a broker (event bus). Observer has none.
- Discovery: Observer requires observers to register on a specific subject; Pub/Sub uses topics to discover messages.
- Testability: Pub/Sub is easier to mock (swap the bus). Observer ties you to a specific subject instance.
- Distribution: Pub/Sub generalizes well to process/server boundaries (Kafka, Redis, SNS). Observer is typically in-process.
Pub/Sub Implementation
// Event channel (broker)
const pubsub = {};
(function (q) {
const topics = {};
let subUid = -1;
// Publish an event
q.publish = function (topic, args) {
if (!topics[topic]) return false;
const subscribers = topics[topic];
subscribers.forEach(sub => sub.func(topic, args));
return true;
};
// Subscribe to an event
q.subscribe = function (topic, func) {
if (!topics[topic]) topics[topic] = [];
const token = (++subUid).toString();
topics[topic].push({ token, func });
return token;
};
// Unsubscribe from a topic
q.unsubscribe = function (token) {
for (const topic in topics) {
if (topics[topic]) {
for (let i = 0; i < topics[topic].length; i++) {
if (topics[topic][i].token === token) {
topics[topic].splice(i, 1);
return token;
}
}
}
}
return false;
};
})(pubsub);
🕸️ Mediator Pattern (Simple "var mediator" example)
📘 One-line Definition:
The Mediator Pattern centralizes communication so objects don't call each other directly; they publish/subscribe via a mediator. Pub Sub in the same class
💻 Basic mediator
var mediator = (function () {
var channels = {};
function subscribe(channel, fn) {
if (!channels[channel]) channels[channel] = [];
channels[channel].push({ context: this, callback: fn });
return this;
}
function publish(channel) {
if (!channels[channel]) return false;
var args = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < channels[channel].length; i++) {
var sub = channels[channel][i];
sub.callback.apply(sub.context, args);
}
return this;
}
function installTo(obj) {
obj.subscribe = subscribe;
obj.publish = publish;
return obj;
}
return { subscribe: subscribe, publish: publish, installTo: installTo };
})();
▶️ Usage (tiny demo)
// Add mediator capabilities to plain objects
var chat = {};
mediator.installTo(chat);
// Subscriber
chat.subscribe("message:new", function (text, from) {
console.log("New message:", text, "from:", from);
});
// Publisher
chat.publish("message:new", "Hello world!", "Alice");
// -> New message: Hello world! from: Alice
⚖️ Pros
- Centralizes interaction logic; peers don't reference each other
- Easy to add/remove participants without rewiring callers
-
installTo
mixes mediator behavior into any object
⚠️ Cons
- The mediator can become a "god object" if it grows unchecked
- Indirection can make flow harder to trace without logging
🧬 Prototype Pattern
📘 One-line Definition:
The Prototype Pattern creates new objects by cloning an existing object (the prototype) instead of instantiating classes, enabling efficient reuse and flexible object composition.
🧩 What It Does
JavaScript natively supports prototypes — every object inherits from another object via its internal [[Prototype]]
.
The Prototype Pattern leverages this feature by using an existing object as a blueprint and creating new objects that delegate to it.
It's useful when:
- You want to avoid repetitive class definitions.
- You need to clone or extend existing objects efficiently.
- You prefer object composition over inheritance hierarchies.
💻 Example
// The prototype object
var vehiclePrototype = {
init: function (carModel) {
this.model = carModel;
},
getModel: function () {
console.log("The model of this vehicle is " + this.model);
}
};
// Create a new object based on vehiclePrototype
function vehicle(model) {
function F() {}
F.prototype = vehiclePrototype;
var f = new F();
f.init(model);
return f;
}
// Usage
var car = vehicle("Ford Escort");
car.getModel(); // The model of this vehicle is Ford Escort
✅ Here, the vehiclePrototype
serves as the blueprint, and every new vehicle instance delegates behavior to it.
⚙️ Modern ES6+ Version
const vehiclePrototype = {
init(model) {
this.model = model;
},
getModel() {
console.log(`The model of this vehicle is ${this.model}`);
}
};
const car = Object.create(vehiclePrototype);
car.init("Tesla Model 3");
car.getModel(); // The model of this vehicle is Tesla Model 3
Object.create(proto)
is the idiomatic modern way to use the Prototype Pattern in JavaScript.
⚖️ Pros
- ✅ Memory-efficient: Methods are shared via the prototype chain.
- ✅ Built-in support: JavaScript's prototype system makes this natural.
- ✅ Simple cloning: Easy to extend existing behavior without class hierarchies.
- ✅ Flexible: Great for lightweight objects or dynamic object creation.
⚠️ Cons
- ❌ Changes to the prototype affect all objects inheriting from it.
- ❌ Deep cloning requires extra handling for nested objects.
- ❌ Debugging prototype chains can be confusing for newcomers.
🕹️ Command Pattern
📘 One-line Definition:
The Command Pattern encapsulates a request as an object (or named command), letting you parameterize, queue, log, and undo operations independently of the callers.
🧩 What It Does
Instead of calling functions directly, clients invoke a single execute
entry point with a command name and arguments.
The invoker looks up and runs the matching operation — cleanly decoupling what to do from who calls it.
💻 Example — carManager
var carManager = {
// concrete commands
requestInfo: function (model, id) {
return "The information for " + model + " with ID " + id + " is being processed.";
},
buyVehicle: function (model, id) {
return "You have successfully purchased Item " + id + ", a " + model + ".";
},
arrangeViewing: function (model, id) {
return "You have successfully booked a viewing for " + model + " (ID: " + id + ").";
}
};
// the invoker
carManager.execute = function (name) {
if (typeof carManager[name] !== "function") {
throw new Error("Command not found: " + name);
}
// forward remaining args to the concrete command
return carManager[name].apply(carManager, Array.prototype.slice.call(arguments, 1));
};
// Usage
carManager.execute("arrangeViewing", "Ferrari", "14523");
// → "You have successfully booked a viewing for Ferrari (ID: 14523)."
carManager.execute("requestInfo", "Tesla Model S", "54321");
// → "The information for Tesla Model S with ID 54321 is being processed."
carManager.execute("buyVehicle", "Ford Mustang", "12345");
// → "You have successfully purchased Item 12345, a Ford Mustang."
execute
acts as the Invoker; each method (e.g., buyVehicle
) is a Command.
Adding a new command = add a new method and call via execute("newCommand", ...)
.
⚙️ Optional: Parameter object variant (cleaner args)
carManager.execute("arrangeViewing", { model: "Ferrari", id: "14523" });
(Modify command signatures to accept a single object: { model, id }
— easier to evolve.)
⚖️ Pros
- ✅ Decoupling: Callers don't need to know which function to call directly.
- ✅ Extendable: Add new commands without changing the invoker's logic.
- ✅ Queue/Log/Undo-ready: Commands are addressable units (can be queued, retried, audited).
- ✅ Uniform interface: One
execute()
to rule them all.
⚠️ Cons
- ❌ Indirection: Tracing call flow requires jumping through the invoker.
- ❌ Overhead: For tiny apps, command routing can feel heavy.
- ❌ Undo complexity: Needs explicit
undo()
or inverse operations (not shown here).
🧱 Facade Pattern
📘 One-line Definition:
The Facade Pattern provides a simplified, unified interface to a complex system — hiding the underlying complexity behind easy-to-use methods.
🧩 What It Does
Think of a facade as a "front desk" — clients interact with it instead of dealing with all the internal departments directly.
In JavaScript, it's often used to:
- Simplify multiple method calls into one.
- Abstract complex libraries or APIs.
- Create cleaner interfaces for modules or services.
💻 Example — Simple Facade
var module = (function () {
var _private = {
i: 5,
get: function () {
console.log("Current value: " + this.i);
},
set: function (val) {
this.i = val;
},
run: function () {
console.log("Running...");
},
jump: function () {
console.log("Jumping...");
}
};
return {
// Facade method — simplifies multiple internal operations
facade: function (args) {
_private.set(args.value);
_private.get();
if (args.run) {
_private.run();
}
}
};
})();
Usage:
module.facade({ value: 10, run: true });
// Output:
// Current value: 10
// Running...
✅ The client doesn't need to know about _private.get()
, _private.set()
, or _private.run()
.
It just calls a single, simple method: facade()
.
⚙️ How It Works
- The facade acts as an intermediary layer.
- It hides multiple operations (set, get, run, etc.) behind a simple, declarative API.
- Internally, it coordinates these actions in the correct order.
⚖️ Pros
- ✅ Simplifies complex subsystems.
- ✅ Improves readability and reduces cognitive load.
- ✅ Encourages separation of concerns — clients use a clean API.
- ✅ Safer abstraction layer — prevents misuse of internal details.
🏭 Factory Pattern
📘 One-line Definition:
The Factory Pattern provides an interface for creating objects without specifying the exact class or constructor to instantiate — it lets subclasses or logic decide which object type to create.
🧩 What It Does
The Factory Pattern abstracts object creation.
Instead of calling constructors directly with new
, you delegate that responsibility to a factory function or class that:
- Decides which object to create,
- Configures it if needed,
- Returns it ready for use.
Use it when:
- Object creation is complex or dynamic.
- You need to instantiate different types of objects at runtime.
- You want to decouple clients from concrete implementations.
💻 Example — VehicleFactory
// Base constructors
function Car(options) {
this.doors = options.doors || 4;
this.state = options.state || "brand new";
this.color = options.color || "silver";
}
function Truck(options) {
this.wheelSize = options.wheelSize || "large";
this.state = options.state || "used";
this.color = options.color || "blue";
}
// Factory
function VehicleFactory() {}
// Default vehicle type
VehicleFactory.prototype.vehicleClass = Car;
VehicleFactory.prototype.createVehicle = function (options) {
switch (options.vehicleType) {
case "car":
this.vehicleClass = Car;
break;
case "truck":
this.vehicleClass = Truck;
break;
default:
this.vehicleClass = Car;
}
return new this.vehicleClass(options);
};
// Usage
const factory = new VehicleFactory();
const car = factory.createVehicle({
vehicleType: "car",
color: "yellow",
doors: 6,
});
const truck = factory.createVehicle({
vehicleType: "truck",
state: "old",
color: "red",
});
console.log(car instanceof Car); // true
console.log(truck instanceof Truck); // true
✅ The factory hides the instantiation details — you just tell it what you need.
⚙️ How It Works
- The factory encapsulates object creation logic.
- You pass configuration data (
options.vehicleType
). - The factory selects the right constructor and returns an instance.
- The client stays agnostic to how the object is built.
⚖️ Pros
- ✅ Encapsulation: Keeps creation logic in one place.
- ✅ Flexibility: Easy to add new types (e.g., Bike) without touching client code.
- ✅ Loose coupling: Clients depend on an interface, not concrete classes.
- ✅ Centralized configuration: Useful for dependency injection and testing.
⚠️ Cons
- ❌ Adds an extra abstraction layer — might be overkill for simple cases.
- ❌ Can hide what's actually being created (less transparency).
- ❌ Too many factory types can complicate maintenance.
🧩 Mixin Pattern
📘 One-line Definition:
The Mixin Pattern lets you add reusable functionality to multiple objects or classes without using inheritance, by "mixing in" shared behavior.
🧩 What It Does
JavaScript supports object composition — meaning we can combine behaviors from multiple sources.
The Mixin Pattern uses this capability to copy or assign methods and properties from one object (the mixin) into another.
Use it when:
- You want to share behavior across classes without forming a deep inheritance chain.
- Multiple objects need similar methods (e.g., logging, event emitting, formatting).
- You prefer composition over inheritance for better flexibility and reusability.
💻 Example
// A simple mixin object
var myMixins = {
moveUp: function () {
console.log("Moving up");
},
moveDown: function () {
console.log("Moving down");
},
stop: function () {
console.log("Stopping");
}
};
// Constructor function
function CarAnimator() {
this.moveLeft = function () {
console.log("Moving left");
};
}
function PersonAnimator() {
this.moveRandomly = function () {
console.log("Moving randomly");
};
}
// Extend function (copies mixin methods to target)
function extend(destination, source) {
for (var key in source) {
if (source.hasOwnProperty(key)) {
destination[key] = source[key];
}
}
}
// Add mixin behavior to constructors
extend(CarAnimator.prototype, myMixins);
extend(PersonAnimator.prototype, myMixins);
// Usage
var myCar = new CarAnimator();
myCar.moveLeft(); // Moving left
myCar.moveDown(); // Moving down
myCar.stop(); // Stopping
var person = new PersonAnimator();
person.moveRandomly(); // Moving randomly
person.moveUp(); // Moving up
✅ CarAnimator
and PersonAnimator
now share moveUp
, moveDown
, and stop
— without extending from a common parent class.
⚙️ How It Works
- Define a mixin object (
myMixins
) with reusable methods. - Use a helper like
extend()
orObject.assign()
to copy those methods into another object or prototype. - Each receiver can now access shared methods independently.
🧠 Modern ES6+ Version
const moveMixin = {
moveUp() { console.log("Moving up"); },
moveDown() { console.log("Moving down"); },
stop() { console.log("Stopping"); },
};
class CarAnimator {
moveLeft() { console.log("Moving left"); }
}
// Mix in shared behavior
Object.assign(CarAnimator.prototype, moveMixin);
const car = new CarAnimator();
car.moveUp(); // Moving up
car.stop(); // Stopping
Object.assign()
is the modern, idiomatic way to mix in functionality in ES6+.
⚖️ Pros
- ✅ Promotes code reuse without deep inheritance.
- ✅ Flexible — combine multiple mixins as needed.
- ✅ Works across unrelated objects or classes.
- ✅ Avoids diamond problems that come with multiple inheritance.
⚠️ Cons
- ❌ Can cause method name collisions if multiple mixins define the same method.
- ❌ Doesn't preserve true encapsulation — copied methods can be overwritten.
- ❌ Harder to trace origin of methods in large systems.
🎁 Decorator Pattern
📘 One-line Definition:
The Decorator Pattern dynamically adds new behavior or responsibilities to an object without modifying its original structure — wrapping it with additional functionality. Basically improves code reuse.
🧩 What It Does
The Decorator Pattern allows you to extend object behavior at runtime instead of using inheritance.
You "decorate" the original object by wrapping it with another object that enhances or modifies its behavior.
Use it when:
- You need to add features dynamically without altering the original code.
- You want flexible and composable behavior instead of rigid inheritance.
- You need to apply multiple independent enhancements to the same object.
💻 Example — MacBook Decorators
// Core component
function MacBook() {
this.cost = function () {
return 997;
};
this.screenSize = function () {
return 11.6;
};
}
// Decorators
function memory(macbook) {
const cost = macbook.cost();
macbook.cost = function () {
return cost + 75;
};
}
function engraving(macbook) {
const cost = macbook.cost();
macbook.cost = function () {
return cost + 200;
};
}
function insurance(macbook) {
const cost = macbook.cost();
macbook.cost = function () {
return cost + 250;
};
}
// Usage
const mb = new MacBook();
memory(mb);
engraving(mb);
insurance(mb);
console.log("Price:", mb.cost()); // Price: 1522
console.log("Screen Size:", mb.screenSize()); // Screen Size: 11.6
✅ Each decorator (memory
, engraving
, insurance
) adds a new cost without changing the MacBook class.
✅ They can be applied independently or combined in any order.
⚙️ How It Works
- Start with a base object — e.g., MacBook.
- Decorators wrap the base object, extending its behavior (like
cost()
in this case). - The original object remains intact, but now has enhanced behavior.
🧠 Modern ES6+ Version
class MacBook {
cost() { return 997; }
screenSize() { return 11.6; }
}
const withMemory = macbook => ({
...macbook,
cost: () => macbook.cost() + 75
});
const withEngraving = macbook => ({
...macbook,
cost: () => macbook.cost() + 200
});
const withInsurance = macbook => ({
...macbook,
cost: () => macbook.cost() + 250
});
// Usage
let myMac = new MacBook();
myMac = withMemory(myMac);
myMac = withEngraving(myMac);
myMac = withInsurance(myMac);
console.log("Total Cost:", myMac.cost()); // 1522
console.log("Screen Size:", myMac.screenSize()); // 11.6
This functional style makes decorators composable and pure — ideal for modern JavaScript apps.
⚖️ Pros
- ✅ Extensible: Add new features without touching existing code.
- ✅ Composability: Combine multiple decorators for custom behavior.
- ✅ Open/Closed Principle: Open for extension, closed for modification.
- ✅ Runtime flexibility: You can apply/remove decorators dynamically.
⚠️ Cons
- ❌ Can make debugging tricky — multiple layers of wrapping.
- ❌ Overuse leads to complex call chains.
- ❌ Slight performance overhead from wrapping functions repeatedly.
🪶 Flyweight Pattern
📘 Definition:
"The Flyweight Pattern is a structural design pattern used to minimize memory usage or computational expenses by sharing as much data as possible with similar objects. It is a way to use objects in large numbers when a simple repeated representation would be too costly."
Basically for repetitive, optimisation slow code, sharing data
🧩 What It Does
When you have many similar objects, each with duplicate data, the Flyweight Pattern helps by sharing common (intrinsic) state and only keeping unique (extrinsic) state per instance.
In other words:
- Intrinsic state → shared across many objects (e.g., coffee type).
- Extrinsic state → unique to each instance (e.g., table number).
It's ideal for optimizing large collections of small, repetitive objects (like DOM nodes, UI icons, particles, etc.).
💻 Minimal Example — Coffee Order System
// Flyweight (shared object)
function CoffeeFlavor(flavorName) {
this.flavor = flavorName;
}
// Flyweight Factory (manages shared instances)
const CoffeeFlavorFactory = (function () {
const flavors = {};
return {
getFlavor(name) {
if (!flavors[name]) {
flavors[name] = new CoffeeFlavor(name);
}
return flavors[name];
},
getCount() {
return Object.keys(flavors).length;
}
};
})();
// Usage
const flavorFactory = CoffeeFlavorFactory;
const order1 = flavorFactory.getFlavor("Cappuccino");
const order2 = flavorFactory.getFlavor("Espresso");
const order3 = flavorFactory.getFlavor("Cappuccino");
console.log(order1 === order3); // true (shared instance)
console.log("Total unique flavors:", flavorFactory.getCount()); // 2
✅ Only two unique CoffeeFlavor objects exist (Cappuccino, Espresso), even though multiple orders use them.
⚙️ Key Idea
- Intrinsic data (flavor) is stored in the shared object.
- Extrinsic data (order details, table number, etc.) stays outside and is passed when needed.
- The factory ensures one shared object per unique intrinsic state.
⚖️ Pros
- ✅ Greatly reduces memory usage when many similar objects exist.
- ✅ Centralized object management (through the factory).
- ✅ Improves performance in object-heavy systems.
⚠️ Cons
- ❌ Adds complexity due to separating intrinsic and extrinsic data.
- ❌ Requires a factory or registry to manage shared instances.
- ❌ Slight overhead in coordinating state between shared and unique data.
Top comments (0)