OOP Patterns in JavaScript
1. Creational Patterns
- The Factory Method
The Factory Method defines an interface for creating an object but leaves the choice of its type to the subclasses, creating instances of classes derived from an abstract class.
// Define an interface (abstract class) for the product
class Car {
displayInfo() {
throw new Error('This method must be overridden by subclasses');
}
}
// Concrete product class 1
class Toyota extends Car {
displayInfo() {
console.log('Toyota Car');
}
}
// Concrete product class 2
class Honda extends Car {
displayInfo() {
console.log('Honda Car');
}
}
// Define the factory method
function createCar(type) {
switch (type) {
case 'Toyota':
return new Toyota();
case 'Honda':
return new Honda();
default:
throw new Error('Invalid car type');
}
}
// Create instances using the factory method
const myToyota = createCar('Toyota');
const myHonda = createCar('Honda');
// Display information about the cars
myToyota.displayInfo();
myHonda.displayInfo();
- The Abstract Factory
The Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes.
// Abstract factory interface
class CarFactory {
createCar() {
throw new Error('This method must be overridden by subclasses');
}
createEngine() {
throw new Error('This method must be overridden by subclasses');
}
}
// Concrete factory for Toyota cars
class ToyotaFactory extends CarFactory {
createCar() {
return new Toyota();
}
createEngine() {
return new ToyotaEngine();
}
}
// Concrete factory for Honda cars
class HondaFactory extends CarFactory {
createCar() {
return new Honda();
}
createEngine() {
return new HondaEngine();
}
}
// Abstract product classes
class Car {
displayInfo() {
throw new Error('This method must be overridden by subclasses');
}
}
class Engine {
start() {
throw new Error('This method must be overridden by subclasses');
}
}
// Concrete product classes for Toyota
class Toyota extends Car {
displayInfo() {
console.log('Toyota Car');
}
}
class ToyotaEngine extends Engine {
start() {
console.log('Toyota Engine starting...');
}
}
// Concrete product classes for Honda
class Honda extends Car {
displayInfo() {
console.log('Honda Car');
}
}
class HondaEngine extends Engine {
start() {
console.log('Honda Engine starting...');
}
}
// Client code using the abstract factory
function useCarFactory(factory) {
const car = factory.createCar();
const engine = factory.createEngine();
car.displayInfo();
engine.start();
}
// Use the Toyota factory
const toyotaFactory = new ToyotaFactory();
useCarFactory(toyotaFactory);
// Use the Honda factory
const hondaFactory = new HondaFactory();
useCarFactory(hondaFactory);
- The Builder
The Builder separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
// Product
class Car {
constructor() {
this.make = null;
this.model = null;
this.year = null;
}
displayInfo() {
console.log(`Make: ${this.make}, Model: ${this.model}, Year: ${this.year}`);
}
}
// Builder interface
class CarBuilder {
constructor() {
this.car = new Car();
}
buildMake(make) {
this.car.make = make;
return this;
}
buildModel(model) {
this.car.model = model;
return this;
}
buildYear(year) {
this.car.year = year;
return this;
}
getResult() {
return this.car;
}
}
// Director
class CarDirector {
constructor(builder) {
this.builder = builder;
}
constructSportsCar(make, model, year) {
return this.builder
.buildMake(make)
.buildModel(model)
.buildYear(year)
.getResult();
}
}
// Client code
const builder = new CarBuilder();
const director = new CarDirector(builder);
const sportsCar = director.constructSportsCar('Ferrari', '488 GTB', 2023);
sportsCar.displayInfo();
- The Prototype
The Prototype involves creating new objects by copying an existing object, known as the prototype. JavaScript has built-in support for prototypes through object cloning.
class CarPrototype {
constructor() {
this.model = 'Generic';
this.make = 'Unknown';
this.year = 'Unknown';
}
displayInfo() {
console.log(`Make: ${this.make}, Model: ${this.model}, Year: ${this.year}`);
}
clone() {
// Create a new object with the same prototype
return Object.create(this);
}
}
// Create a specific car instance by cloning the prototype
const myCar = new CarPrototype();
myCar.make = 'Toyota';
myCar.model = 'Camry';
myCar.year = 2022;
// Display information about the specific car
myCar.displayInfo();
// Create another car instance by cloning the prototype
const anotherCar = myCar.clone();
anotherCar.model = 'Corolla';
anotherCar.year = 2023;
// Display information about the other car
anotherCar.displayInfo();
- The Singleton
The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance.
class Singleton {
constructor() {
if (!Singleton.instance) {
// If an instance doesn't exist, create one
this.value = 'Singleton Instance';
Singleton.instance = this;
}
// Return the existing instance
return Singleton.instance;
}
}
// Usage
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1.value); // Output: Singleton Instance
console.log(instance1 === instance2); // Output: true
2. Structural Patterns
- The Adapter
The Adapter allows incompatible interfaces to work together.
// Adaptee: Old system with an incompatible interface
class OldSystem {
request() {
return 'Old System Request';
}
}
// Target: New system with a different interface
class NewSystem {
specificRequest() {
return 'New System Specific Request';
}
}
// Adapter: Adapts the OldSystem interface to match the NewSystem interface
class Adapter {
constructor(oldSystem) {
this.oldSystem = oldSystem;
}
request() {
return this.oldSystem.request();
}
}
// Client code using the NewSystem interface
function useNewSystem(system) {
console.log(system.specificRequest());
}
// Usage
const oldSystem = new OldSystem();
const adaptedSystem = new Adapter(oldSystem);
// Using the NewSystem interface with the adapted system
useNewSystem(adaptedSystem);
- The Bridge
The Bridge separates abstraction from implementation so that the two can vary independently.
// Abstraction
class Vehicle {
constructor(make, model, implementor) {
this.make = make;
this.model = model;
this.implementor = implementor;
}
displayInfo() {
console.log(`Make: ${this.make}, Model: ${this.model}`);
this.implementor.showDetails();
}
}
// Implementor interface
class VehicleImplementor {
showDetails() {
throw new Error('This method must be overridden by subclasses');
}
}
// Concrete Implementors
class CarImplementor extends VehicleImplementor {
showDetails() {
console.log('Vehicle Type: Car');
}
}
class BikeImplementor extends VehicleImplementor {
showDetails() {
console.log('Vehicle Type: Bike');
}
}
// Usage
const car = new Vehicle('Toyota', 'Camry', new CarImplementor());
const bike = new Vehicle('Harley', 'Davidson', new BikeImplementor());
car.displayInfo();
bike.displayInfo();
- The Composite
The Composite lets you compose objects into tree structures to represent part-whole hierarchies, creates a tree structure with leaves and composites and then displaying the entire structure. This allows you to represent complex structures as a combination of simple and composite objects.
Component
is the component interface that declares the common interface for all concrete classes (both leaf and composite).Leaf
is a leaf node that represents individual objects in the composition.Composite
is a composite node that can have children (either leaves or other composites). It implements theComponent
interface and provides methods for managing its children.
// Component interface
class Component {
constructor(name) {
this.name = name;
}
display() {
throw new Error('This method must be overridden by subclasses');
}
}
// Leaf
class Leaf extends Component {
display() {
console.log(`Leaf: ${this.name}`);
}
}
// Composite
class Composite extends Component {
constructor(name) {
super(name);
this.children = [];
}
add(child) {
this.children.push(child);
}
remove(child) {
const index = this.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
}
}
display() {
console.log(`Composite: ${this.name}`);
this.children.forEach(child => child.display());
}
}
// Usage
const leaf1 = new Leaf('Leaf 1');
const leaf2 = new Leaf('Leaf 2');
const leaf3 = new Leaf('Leaf 3');
const composite1 = new Composite('Composite 1');
composite1.add(leaf1);
composite1.add(leaf2);
const composite2 = new Composite('Composite 2');
composite2.add(leaf3);
const rootComposite = new Composite('Root Composite');
rootComposite.add(composite1);
rootComposite.add(composite2);
// Display the entire structure
rootComposite.display();
- The Decorator
The Decorator allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.
// Component interface
class Coffee {
cost() {
return 5;
}
}
// Concrete component
class SimpleCoffee extends Coffee {
cost() {
return super.cost();
}
}
// Decorator
class CoffeeDecorator extends Coffee {
constructor(coffee) {
super();
this._coffee = coffee;
}
cost() {
return this._coffee.cost();
}
}
// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
constructor(coffee) {
super(coffee);
}
cost() {
return super.cost() + 2;
}
}
class SugarDecorator extends CoffeeDecorator {
constructor(coffee) {
super(coffee);
}
cost() {
return super.cost() + 1;
}
}
// Usage
const simpleCoffee = new SimpleCoffee();
console.log('Cost of Simple Coffee:', simpleCoffee.cost());
const milkCoffee = new MilkDecorator(simpleCoffee);
console.log('Cost of Milk Coffee:', milkCoffee.cost());
const sweetMilkCoffee = new SugarDecorator(milkCoffee);
console.log('Cost of Sweet Milk Coffee:', sweetMilkCoffee.cost());
- The Facade
The Facade provides a simplified interface to a set of interfaces in a subsystem.
// Subsystem components
class Engine {
start() {
console.log('Engine started');
}
stop() {
console.log('Engine stopped');
}
}
class Lights {
turnOn() {
console.log('Lights turned on');
}
turnOff() {
console.log('Lights turned off');
}
}
class FuelSystem {
pumpFuel() {
console.log('Fuel pumped');
}
stopFuelPump() {
console.log('Fuel pump stopped');
}
}
// Facade class
class CarFacade {
constructor() {
this.engine = new Engine();
this.lights = new Lights();
this.fuelSystem = new FuelSystem();
}
startCar() {
this.engine.start();
this.lights.turnOn();
this.fuelSystem.pumpFuel();
console.log('Car started and ready to go!');
}
stopCar() {
console.log('Stopping the car...');
this.engine.stop();
this.lights.turnOff();
this.fuelSystem.stopFuelPump();
console.log('Car stopped');
}
}
// Usage
const carFacade = new CarFacade();
// Starting the car
carFacade.startCar();
// Stopping the car
carFacade.stopCar();
- The Flyweight
The Flyweight minimizes memory or computational overhead by sharing as much as possible with related objects. It is often used to manage a large number of similar objects efficiently. The example demonstrates creating orders for different coffee flavors, and you can see that even though multiple orders are made for the same flavor, the flyweight pattern ensures that only one instance of each flavor is created, reducing memory usage.
// Flyweight factory
class CoffeeFlavorFactory {
constructor() {
this.flavors = {};
}
getFlavor(flavorName) {
if (!this.flavors[flavorName]) {
this.flavors[flavorName] = new CoffeeFlavor(flavorName);
}
return this.flavors[flavorName];
}
getTotalFlavors() {
return Object.keys(this.flavors).length;
}
}
// Flyweight
class CoffeeFlavor {
constructor(flavor) {
this.flavor = flavor;
}
serveCoffee(context) {
console.log(`Serving coffee flavor ${this.flavor} to ${context}`);
}
}
// Usage
const coffeeFlavorFactory = new CoffeeFlavorFactory();
const order1 = coffeeFlavorFactory.getFlavor('Cappuccino');
order1.serveCoffee('John');
const order2 = coffeeFlavorFactory.getFlavor('Latte');
order2.serveCoffee('Jane');
const order3 = coffeeFlavorFactory.getFlavor('Cappuccino');
order3.serveCoffee('Bob');
console.log(`Total CoffeeFlavor objects created: ${coffeeFlavorFactory.getTotalFlavors()}`);
- The Proxy
The Proxy provides a surrogate or placeholder for another object to control access to it.
// Subject interface
class RealSubject {
request() {
console.log('RealSubject: Handling request');
}
}
// Proxy class
class ProxySubject {
constructor(realSubject) {
this.realSubject = realSubject;
}
request() {
if (this.checkAccess()) {
this.realSubject.request();
} else {
console.log('ProxySubject: Access denied');
}
}
checkAccess() {
// Simulate access control logic
return Math.random() > 0.5; // Access granted if a random number is greater than 0.5
}
}
// Usage
const realSubject = new RealSubject();
const proxy = new ProxySubject(realSubject);
// Access granted
proxy.request();
// Access denied
proxy.request();
3. Behavioral Patterns
- The Chain of Responsibility
The Chain of Responsibility passes requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.
// Handler interface
class Handler {
constructor(successor = null) {
this.successor = successor;
}
handleRequest(request) {
if (this.successor) {
this.successor.handleRequest(request);
} else {
console.log('Request not handled');
}
}
}
// Concrete Handlers
class ConcreteHandler1 extends Handler {
handleRequest(request) {
if (request === 'Handler1') {
console.log('ConcreteHandler1: Handling request');
} else {
super.handleRequest(request);
}
}
}
class ConcreteHandler2 extends Handler {
handleRequest(request) {
if (request === 'Handler2') {
console.log('ConcreteHandler2: Handling request');
} else {
super.handleRequest(request);
}
}
}
class ConcreteHandler3 extends Handler {
handleRequest(request) {
if (request === 'Handler3') {
console.log('ConcreteHandler3: Handling request');
} else {
super.handleRequest(request);
}
}
}
// Usage
const handler1 = new ConcreteHandler1();
const handler2 = new ConcreteHandler2();
const handler3 = new ConcreteHandler3();
// Set up the chain of responsibility
handler1.successor = handler2;
handler2.successor = handler3;
// Send requests through the chain
handler1.handleRequest('Handler1');
handler1.handleRequest('Handler2');
handler1.handleRequest('Handler3');
handler1.handleRequest('Handler4');
- The Command
The Command turns a request into an autonomous object containing all information about the request. This object can then be passed around and manipulated like any other object.
// Command interface
class Command {
execute() {
throw new Error('This method must be overridden by subclasses');
}
}
// Concrete Command
class LightOnCommand extends Command {
constructor(light) {
super();
this.light = light;
}
execute() {
this.light.turnOn();
}
}
// Receiver
class Light {
turnOn() {
console.log('Light is ON');
}
turnOff() {
console.log('Light is OFF');
}
}
// Invoker
class RemoteControl {
constructor() {
this.command = null;
}
setCommand(command) {
this.command = command;
}
pressButton() {
this.command.execute();
}
}
// Usage
const light = new Light();
const lightOnCommand = new LightOnCommand(light);
const remoteControl = new RemoteControl();
remoteControl.setCommand(lightOnCommand);
remoteControl.pressButton();
- The Iterator
The Iterator provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
// Iterator interface
class Iterator {
constructor(items) {
this.index = 0;
this.items = items;
}
hasNext() {
return this.index < this.items.length;
}
next() {
return this.hasNext() ? this.items[this.index++] : null;
}
}
// Aggregate interface
class Aggregate {
createIterator() {
throw new Error('This method must be overridden by subclasses');
}
}
// Concrete Iterator
class ConcreteIterator extends Iterator {
constructor(items) {
super(items);
}
}
// Concrete Aggregate
class ConcreteAggregate extends Aggregate {
constructor() {
super();
this.items = [];
}
addItem(item) {
this.items.push(item);
}
createIterator() {
return new ConcreteIterator(this.items);
}
}
// Usage
const aggregate = new ConcreteAggregate();
aggregate.addItem('Item 1');
aggregate.addItem('Item 2');
aggregate.addItem('Item 3');
const iterator = aggregate.createIterator();
while (iterator.hasNext()) {
console.log(iterator.next());
}
- The Mediator
The Mediator defines an object (the mediator) that centralizes communication between other objects (colleagues) without them needing to be directly aware of each other.
// Mediator
class ChatMediator {
constructor() {
this.colleagues = [];
}
addColleague(colleague) {
this.colleagues.push(colleague);
}
sendMessage(message, sender) {
this.colleagues.forEach(colleague => {
if (colleague !== sender) {
colleague.receiveMessage(message);
}
});
}
}
// Colleague
class Colleague {
constructor(mediator, name) {
this.mediator = mediator;
this.name = name;
mediator.addColleague(this);
}
send(message) {
console.log(`${this.name} sends: ${message}`);
this.mediator.sendMessage(message, this);
}
receiveMessage(message) {
console.log(`${this.name} receives: ${message}`);
}
}
// Usage
const mediator = new ChatMediator();
const colleague1 = new Colleague(mediator, 'Colleague 1');
const colleague2 = new Colleague(mediator, 'Colleague 2');
const colleague3 = new Colleague(mediator, 'Colleague 3');
colleague1.send('Hello, everyone!');
colleague2.send('Hi there!');
colleague3.send('Greetings!');
- The Memento
The Memento allows an object’s state to be captured and restored.
// Memento
class Memento {
constructor(state) {
this.state = state;
}
getState() {
return this.state;
}
}
// Originator
class Originator {
constructor() {
this.state = null;
}
setState(state) {
this.state = state;
}
saveStateToMemento() {
return new Memento(this.state);
}
getStateFromMemento(memento) {
this.state = memento.getState();
}
displayState() {
console.log(`Current State: ${this.state}`);
}
}
// Caretaker
class Caretaker {
constructor() {
this.mementos = [];
}
addMemento(memento) {
this.mementos.push(memento);
}
getMemento(index) {
return this.mementos[index];
}
}
// Usage
const originator = new Originator();
const caretaker = new Caretaker();
// Set state and save to memento
originator.setState('State 1');
caretaker.addMemento(originator.saveStateToMemento());
// Set another state and save to memento
originator.setState('State 2');
caretaker.addMemento(originator.saveStateToMemento());
// Display current state
originator.displayState();
// Restore to the first state
originator.getStateFromMemento(caretaker.getMemento(0));
// Display restored state
originator.displayState();
- The Observer
The Observer is a behavioral design pattern where an object, known as the subject, maintains a list of its dependents, called observers, that are notified of state changes, typically by calling one of their methods.
Observer
is the observer interface that declares theupdate
method.Subject
is the subject class that maintains a list of observers and provides methods to add, remove, and notify observers.ConcreteObserver
is a concrete observer that implements theupdate
method to receive updates.ConcreteSubject
is a concrete subject that extendsSubject
and notifies observers when its data changes.
The example demonstrates creating observers, adding them to the subject, and notifying them when the subject’s data changes. The observers receive updates and can react accordingly.
// Observer interface
class Observer {
update(data) {
throw new Error('This method must be overridden by subclasses');
}
}
// Subject
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
notifyObservers(data) {
this.observers.forEach(observer => observer.update(data));
}
}
// Concrete Observer
class ConcreteObserver extends Observer {
constructor(name) {
super();
this.name = name;
}
update(data) {
console.log(`${this.name} received update with data: ${data}`);
}
}
// Concrete Subject
class ConcreteSubject extends Subject {
setData(data) {
console.log(`Setting data to: ${data}`);
this.notifyObservers(data);
}
}
// Usage
const observer1 = new ConcreteObserver('Observer 1');
const observer2 = new ConcreteObserver('Observer 2');
const subject = new ConcreteSubject();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.setData('New Data 1');
subject.setData('New Data 2');
// Output:
// Setting data to: New Data 1
// Observer 1 received update with data: New Data 1
// Observer 2 received update with data: New Data 1
// Setting data to: New Data 2
// Observer 1 received update with data: New Data 2
// Observer 2 received update with data: New Data 2
- The State
The State allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
// State interface
class State {
handle(context) {
throw new Error('This method must be overridden by subclasses');
}
}
// Concrete States
class StateA extends State {
handle(context) {
console.log('Handling request in State A');
context.setState(new StateB());
}
}
class StateB extends State {
handle(context) {
console.log('Handling request in State B');
context.setState(new StateA());
}
}
// Context
class Context {
constructor() {
this.state = new StateA(); // Initial state
}
setState(state) {
this.state = state;
}
request() {
this.state.handle(this);
}
}
// Usage
const context = new Context();
context.request(); // Output: Handling request in State A
context.request(); // Output: Handling request in State B
context.request(); // Output: Handling request in State A
- The Strategy
The Strategy defines a family of algorithms, encapsulates each algorithm, and makes them interchangeable. It allows the client to choose the appropriate algorithm at runtime.
// Strategy interface
class PaymentStrategy {
pay(amount) {
throw new Error('This method must be overridden by subclasses');
}
}
// Concrete Strategies
class CreditCardPayment extends PaymentStrategy {
constructor(cardNumber, expirationDate) {
super();
this.cardNumber = cardNumber;
this.expirationDate = expirationDate;
}
pay(amount) {
console.log(`Paid $${amount} with credit card ${this.cardNumber}`);
}
}
class PayPalPayment extends PaymentStrategy {
constructor(email) {
super();
this.email = email;
}
pay(amount) {
console.log(`Paid $${amount} with PayPal account ${this.email}`);
}
}
// Context
class ShoppingCart {
constructor(paymentStrategy) {
this.paymentStrategy = paymentStrategy;
this.items = [];
}
addItem(item) {
this.items.push(item);
}
calculateTotal() {
return this.items.reduce((total, item) => total + item.price, 0);
}
checkout() {
const totalAmount = this.calculateTotal();
this.paymentStrategy.pay(totalAmount);
}
}
// Usage
const creditCardPayment = new CreditCardPayment('1234-5678-9012-3456', '12/23');
const payPalPayment = new PayPalPayment('john.doe@example.com');
const shoppingCart1 = new ShoppingCart(creditCardPayment);
shoppingCart1.addItem({ name: 'Item 1', price: 25 });
shoppingCart1.addItem({ name: 'Item 2', price: 30 });
shoppingCart1.checkout();
const shoppingCart2 = new ShoppingCart(payPalPayment);
shoppingCart2.addItem({ name: 'Item 3', price: 15 });
shoppingCart2.checkout();
- The Template Method
The Template Method defines the structure of an algorithm in the superclass but lets subclasses override specific steps of the algorithm without changing its structure.
// Abstract Template
class AbstractClass {
templateMethod() {
this.stepOne();
this.stepTwo();
this.stepThree();
}
stepOne() {
throw new Error('This method must be overridden by subclasses');
}
stepTwo() {
throw new Error('This method must be overridden by subclasses');
}
stepThree() {
throw new Error('This method must be overridden by subclasses');
}
}
// Concrete Class
class ConcreteClass extends AbstractClass {
stepOne() {
console.log('ConcreteClass: Step One');
}
stepTwo() {
console.log('ConcreteClass: Step Two');
}
stepThree() {
console.log('ConcreteClass: Step Three');
}
}
// Usage
const concreteObject = new ConcreteClass();
concreteObject.templateMethod();
- The Visitor
The Visitor lets to define a new operation without changing the classes of the elements on which it operates.
// Element interface
class Visitable {
accept(visitor) {
throw new Error('This method must be overridden by subclasses');
}
}
// Concrete Elements
class ConcreteElementA extends Visitable {
accept(visitor) {
visitor.visitConcreteElementA(this);
}
operationA() {
console.log('Operation A in ConcreteElementA');
}
}
class ConcreteElementB extends Visitable {
accept(visitor) {
visitor.visitConcreteElementB(this);
}
operationB() {
console.log('Operation B in ConcreteElementB');
}
}
// Visitor interface
class Visitor {
visitConcreteElementA(element) {
throw new Error('This method must be overridden by subclasses');
}
visitConcreteElementB(element) {
throw new Error('This method must be overridden by subclasses');
}
}
// Concrete Visitor
class ConcreteVisitor extends Visitor {
visitConcreteElementA(element) {
console.log('Visitor is operating on ConcreteElementA');
element.operationA();
}
visitConcreteElementB(element) {
console.log('Visitor is operating on ConcreteElementB');
element.operationB();
}
}
// Usage
const elements = [new ConcreteElementA(), new ConcreteElementB()];
const visitor = new ConcreteVisitor();
elements.forEach(element => {
element.accept(visitor);
});