
In the ever-evolving realm of web development, JavaScript remains a steadfast cornerstone. As this versatile scripting language continues to shape the digital landscape, mastering its intricacies becomes paramount for any developer seeking to craft robust and maintainable applications. Enter the world of design patterns in JavaScript, a fascinating and indispensable toolkit that empowers developers to architect scalable, efficient, and error-resistant code. In this article, we embark on a journey to unravel the secrets of design patterns in JavaScript, exploring their significance, implementation, and real-world applications. Whether you’re a seasoned developer looking to deepen your knowledge or a newcomer eager to grasp the foundations, fasten your seatbelt as we delve into the art of designing with purpose in the world of JavaScript.
Creational.
Creational patterns are a design pattern category and are granted to common problems related to the Creation of Objects situations.
Singleton.
Singleton pattern limits the number of instances of a particular object to just one. Singletons reduce the need for global variables which avoids the risk of name collisions.
In this example, we are checking in the constructor() if an Animal instance exists already or if we need to create a new one.
class Animal { constructor() { if (typeof Animal.instance === 'object') { return Animal.instance; } Animal.instance = this; return this; } } export default Animal;
Use this pattern when you need only one instance of a class.
Prototype.

Prototype pattern creates new objects based on an existent one with default properties values.
In this example, we can use the clone() method to create a new object Fruit with the same name and weight that its parent.
class Fruit { constructor(name, weight) { this.name = name; this.weight = weight; } clone() { return new Fruit(this.name, this.weight); } } export default Fruit;
Use this pattern when you only can instantiate classes in runtime.
Factory.

Factory pattern creates new objects delegating which class to instantiate in subclasses.
In this example, MovieFactory decides what kind of Movie to create.
class MovieFactory { create(genre) { if (genre === 'Adventure') return new Movie(genre, 10000); if (genre === 'Action') return new Movie(genre, 11000); } } class Movie { constructor(type, price) { this.type = type; this.price = price; } } export default MovieFactory;
Use this pattern when you want that a subclass decides what object to create.
Abstract Factory.

The Abstract factory creates new objects grouped by theme without specifying their concrete classes.
function foodProducer(kind) { if (kind === 'protein') return proteinPattern; if (kind === 'fat') return fatPattern; return carbohydratesPattern; } function proteinPattern() { return new Protein(); } function fatPattern() { return new Fat(); } function carbohydratesPattern() { return new Carbohydrates(); } class Protein { info() { return 'I am Protein.'; } } class Fat { info() { return 'I am Fat.'; } } class Carbohydrates { info() { return 'I am carbohydrates.'; } } export default foodProducer;
Use this pattern when the system should be independent of how what it is producing is structured or represented.
Structural.
Structural patterns are a design pattern category and are granted to common problems related to the Composition of Objects and Classes situations.
Adapter.
The adapter pattern allows classes to work together creating a class interface into another one.
In this example, we are using a SoldierAdapter to can use the legacy method attack() in our current system and can support the new version of soldiers SuperSoldiers.
class Soldier { constructor(level) { this.level = level; } attack() { return this.level * 1; } } class SuperSoldier { constructor(level) { this.level = level; } attackWithShield() { return this.level * 10; } } class SoldierAdapter { constructor(superSoldier) { this.superSoldier = superSoldier; } attack() { return this.superSoldier.attackWithShield(); } } export { Soldier, SuperSoldier, SoldierAdapter };
Use this pattern when you need to use existing classes but their interfaces don’t match between them.
Bridge.

Bridge pattern allows having one interface in our class build different implementations depending on what instance we are receiving and what instance we need to return.
In this example, we create a bridge between types of Soldiers and types of Weapons, in this way we can pass the instance of weapons correctly to our soldiers.
class Soldier { constructor(weapon) { this.weapon = weapon; } } class SuperSoldier extends Soldier { constructor(weapon) { super(weapon); } attack() { return 'SuperSoldier, Weapon: ' + this.weapon.get(); } } class IronMan extends Soldier { constructor(weapon) { super(weapon); } attack() { return 'Ironman, Weapon: ' + this.ink.get(); } } class Weapon { constructor(type) { this.type = type; } get() { return this.type; } } class Shield extends Weapon { constructor() { super('shield'); } } class Rocket extends Weapon { constructor() { super('rocket'); } } export { SuperSoldier, IronMan, Shield, Rocket };
Use this pattern when you need to use a specific implementation in runtime from an abstraction.
Composite.

The composite pattern allows the creation of objects with properties that are primitive items or a collection of objects. Each item in the collection can hold other collections themselves, creating deeply nested structures.
In this example, we are creating a computing equipment subsystem that is stored in a Cabinet each element can be a different instance.
//Equipment class Equipment { getPrice() { return this.price || 0; } getName() { return this.name; } setName(name) { this.name = name; } } class Pattern extends Equipment { constructor() { super(); this.equipments = []; } add(equipment) { this.equipments.push(equipment); } getPrice() { return this.equipments .map(equipment => { return equipment.getPrice(); }) .reduce((a, b) => { return a + b; }); } } class Cabbinet extends Pattern { constructor() { super(); this.setName('cabbinet'); } } // --- leafs --- class FloppyDisk extends Equipment { constructor() { super(); this.setName('Floppy Disk'); this.price = 70; } } class HardDrive extends Equipment { constructor() { super(); this.setName('Hard Drive'); this.price = 250; } } class Memory extends Equipment { constructor() { super(); this.setName('Memory'); this.price = 280; } } export { Cabbinet, FloppyDisk, HardDrive, Memory };
Use this pattern when you want to represent hierarchies of objects.
Decorator.

The decorator pattern allows extending objects’ behavior dynamically in runtime.
In this example, we are using a decorator to extend behavior in Facebook Notifications.
class Notification { constructor(kind) { this.kind = kind || "Generic"; } getInfo() { return `I'm a ${this.kind} Notification`; } } class FacebookNotification extends Notification { constructor() { super("Facebook"); } setNotification(msg) { this.message = msg; } getInfo() { return `${super.getInfo()} with the message: ${this.message}`; } } class SMSNotification extends Notification { constructor() { super("SMS"); } getInfo() { return super.getInfo(); } } export { FacebookNotification, SMSNotification };
Use this pattern when you want to add extensions to an object in runtime without affecting other objects.
Facade.

The facade pattern provides a simplified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use
In this example, we are creating a simple interface Cart that abstracts all the complexity from several subsystems as Discount, Shipping, and Fees.
class Cart { constructor() { this.discount = new Discount(); this.shipping = new Shipping(); this.fees = new Fees(); } calc(price) { price = this.discount.calc(price); price = this.fees.calc(price); price += this.shipping.calc(); return price; } } class Discount { calc(value) { return value * 0.85; } } class Shipping { calc() { return 500; } } class Fees { calc(value) { return value * 1.1; } } export default Cart;
Use this pattern when you want to provide a simple interface to a complex subsystem.
Flyweight.

The Flyweight pattern conserves memory by sharing large numbers of fine-grained objects efficiently. Shared flyweight objects are immutable, that is, they cannot be changed as they represent the characteristics that are shared with other objects.
In this example we are managing and creating ingredients for some kind of recipe or culinary application.
class Ingredient { constructor(name) { this.name = name; } getInfo() { return `I'm a ${this.name}` } } class Ingredients { constructor() { this.ingredients = {}; } create(name) { let ingredient = this.ingredients[name]; if (ingredient) return ingredient; this.ingredients[name] = new Ingredient(name); return this.ingredients[name]; } } export { Ingredients };
Use this pattern when an application uses a lot of small objects and their storage is expensive or their identity is not important.
Proxy.

The Proxy pattern provides a surrogate or placeholder object for another object and controls access to this other object.
In this example we are using this pattern to constrain the age of the pilots.
class Plane { fly() { return 'flying'; } } class PilotProxy { constructor(pilot) { this.pilot = pilot; } fly() { return this.pilot.age < 18 ? `too young to fly` : new Plane().fly(); } } class Pilot { constructor(age) { this.age = age; } } export { Plane, PilotProxy, Pilot };
Use this pattern when an object is severely constrained and cannot live up to its responsibility.
Behavioral.
Behavioral patterns are a design pattern category and are granted to common problems related to Communication and the assignment between objects situations.
Chain of Responsibility.
The chain of responsibility pattern allows passing requests along a chain of objects that have a chance to handle the request. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.
In this example, we are chaining the class Discount to handle the request of how much the discount in a shopping cart is.
class ShoppingCart { constructor() { this.products = []; } addProduct(p) { this.products.push(p); } } class Discount { calc(products) { let ndiscount = new NumberDiscount(); let pdiscount = new PriceDiscount(); let none = new NoneDiscount(); ndiscount.setNext(pdiscount); pdiscount.setNext(none); return ndiscount.exec(products); } } class NumberDiscount { constructor() { this.next = null; } setNext(fn) { this.next = fn; } exec(products) { let result = 0; if (products.length > 3) result = 0.05; return result + this.next.exec(products); } } class PriceDiscount { constructor() { this.next = null; } setNext(fn) { this.next = fn; } exec(products) { let result = 0; let total = products.reduce((a, b) => (a + b), 0); if (total >= 500) result = 0.1; return result + this.next.exec(products); } } class NoneDiscount { exec() { return 0; } } export { ShoppingCart, Discount };
Use this pattern when more than one object can handle a request and that information is known in runtime.
Command.

The command pattern allows encapsulation of a request as an object. This transformation lets you pass requests as a method arguments, delay or queue a request’s execution, and support undoable operations.
In this example, we are encapsulating the instructions of on/off as objects and pass them as arguments in the Car constructor.
class Car { constructor(instruction) { this.instruction = instruction; } execute() { this.instruction.execute(); } } class Engine { constructor() { this.state = false; } on() { this.state = true; } off() { this.state = false; } } class OnInstruction { constructor(engine) { this.engine = engine; } execute() { this.engine.on(); } } class OffInstruction { constructor(engine) { this.engine = engine; } execute() { this.engine.off(); } } export { Car, Engine, OnInstruction, OffInstruction };
Use this pattern when you have a queue of requests to handle or you want to have an undo action.
Interpreter.

The interpreter pattern allows creating a grammar for simple language when a problem occurs very often; it could be considered to represent it as a sentence in a simple language so that an interpreter can solve the problem by interpreting the sentence.
In this example, we are creating a simple number to multiply and power a couple of numbers.
class Mul { constructor(left, right) { this.left = left; this.right = right; } interpreter() { return this.left.interpreter() * this.right.interpreter(); } } class Pow { constructor(left, right) { this.left = left; this.right = right; } interpreter() { return this.left.interpreter() - this.right.interpreter(); } } class Num { constructor(val) { this.val = val; } interpreter() { return this.val; } } export { Num, Mul, Pow };
Use this pattern when you want to interpret given language and you can represent statements as abstract syntax trees.
Iterator.

The iterator pattern allows access to the elements in a collection without exposing its underlying representation.
In this example, we are creating a simple iterator that going to have an array with elements, and using the methods next() and hasNext() we can iterate through all the elements.
class Iterator { constructor(el) { this.index = 0; this.elements = el; } next() { return this.elements[this.index++]; } hasNext() { return this.index < this.elements.length; } } export default Iterator;
Use this pattern when you want to access an object’s content collections without knowing how it is internally represented.
Mediator.

The mediator pattern allows reducing chaotic dependencies between objects by defining an object that encapsulates how a set of objects interact.
In this example, we are creating a class mediator TrafficTower that going to allow us now all the positions from Airplane instances.
class TrafficTower { constructor() { this.airplanes = []; } getPositions() { return this.airplanes.map(airplane => { return airplane.position.showPosition(); }); } } class Airplane { constructor(position, trafficTower) { this.position = position; this.trafficTower = trafficTower; this.trafficTower.airplanes.push(this); } getPositions() { return this.trafficTower.getPositions(); } } class Position { constructor(x,y) { this.x = x; this.y = y; } showPosition() { return `My Position is ${x} and ${y}`; } } export { TrafficTower, Airplane, Position };
Use this pattern when a set of objects communicate between them but in complex ways.
Memento.

The memento pattern allows capture and externalizes an object’s internal state so that the object can be restored to this state later.
In this example, we are creating a simple way to store values and restore a snapshot when we need it.
class Memento { constructor(value) { this.value = value; } } const originator = { store: function(val) { return new Memento(val); }, restore: function(memento) { return memento.value; } }; class Keeper { constructor() { this.values = []; } addMemento(memento) { this.values.push(memento); } getMemento(index) { return this.values[index]; } } export { originator, Keeper };
Use this pattern when you want to produce snapshots of the object’s state to be able to restore a previous state of the object.
Observer.
The observer pattern allows defining a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
In this example, we are creating a simple class product that other classes can observe registering about changes in the register() method, and when something is updated notifyAll() method going to communicate with all the observers about these changes.
class ObservedProduct { constructor() { this.price = 0; this.actions = []; } setBasePrice(val) { this.price = val; this.notifyAll(); } register(observer) { this.actions.push(observer); } unregister(observer) { this.actions.remove.filter(function(el) { return el !== observer; }); } notifyAll() { return this.actions.forEach( function(el) { el.update(this); }.bind(this) ); } } class fees { update(product) { product.price = product.price * 1.2; } } class profit { update(product) { product.price = product.price * 2; } } export { ObservedProduct, fees, profit };
Use this pattern when changes to the state of one object may require changing other objects, and the actual set of objects is unknown beforehand or changes dynamically.
State.

The state pattern allows an object to alter its behavior when its internal state changes.
In this example, we are creating a simple state pattern with an Order class that going to update the status with the next() method.
class OrderStatus { constructor(name, nextStatus) { this.name = name; this.nextStatus = nextStatus; } next() { return new this.nextStatus(); } } class WaitingForPayment extends OrderStatus { constructor() { super('waitingForPayment', Shipping); } } class Shipping extends OrderStatus { constructor() { super('shipping', Delivered); } } class Delivered extends OrderStatus { constructor() { super('delivered', Delivered); } } class Order { constructor() { this.state = new WaitingForPayment(); } nextPattern() { this.state = this.state.next(); } } export default Order;
Use this pattern when the object’s behavior depends on its state and its behavior changes in runtime depends on that state.
Strategy.

The strategy pattern allows defining a family of algorithms, encapsulate each one, and make them interchangeable.
In this example, We have a set of discounts that can apply in a ShoppingCart the trick here is that we can pass the function that we gonna apply to the constructor and in that way change the amount discounted.
class ShoppingCart { constructor(discount) { this.discount = discount; this.amount = 0; } checkout() { return this.discount(this.amount); } setAmount(amount) { this.amount = amount; } } function guest(amount) { return amount; } function regular(amount) { return amount * 0.9; } function premium(amount) { return amount * 0.8; } export { ShoppingCart, guest, regular, premium };
Use this pattern when you have a lot of similar classes that only differ in the way they execute some behavior.
Template.

The template pattern allows defining the skeleton of an algorithm in the superclass but lets subclasses override specific steps of the algorithm without changing its structure.
In this example, We are creating a simple template method to calculate taxes and extending this template in VAT and GST (type of taxes), in this way we can reuse the same structure in several taxes classes.
class Tax { calc(value) { if (value >= 1000) value = this.overThousand(value); return this.complementaryFee(value); } complementaryFee(value) { return value + 10; } } class VAT extends Tax { constructor() { super(); } overThousand(value) { return value * 1.1; } } class GST extends Tax { constructor() { super(); } overThousand(value) { return value * 1.2; } } export { VAT, GST };
Use this pattern when you want to let clients extend only particular steps of an algorithm, but not the whole algorithm or its structure.
Visitor.

The visitor pattern allows separate algorithms from the objects on which they operate.
In this example, we are creating a structure to calculate the bonus for two types of employees, in this way we can extend the bonus method to more and more types of employees as CEO bonus, VP bonus, and so on.
function bonusPattern(employee) { if (employee instanceof Manager) employee.bonus = employee.salary * 2; if (employee instanceof Developer) employee.bonus = employee.salary; } class Employee { constructor(salary) { this.bonus = 0; this.salary = salary; } accept(item) { item(this); } } class Manager extends Employee { constructor(salary) { super(salary); } } class Developer extends Employee { constructor(salary) { super(salary); } } export { Developer, Manager, bonusPattern };
Use this pattern when an object structure includes many classes and you want to perform operations on the elements of that structure that depend on their classes.
In this exploration of design patterns in JavaScript, we’ve journeyed through the intricate world of software architecture and engineering. We’ve seen how design patterns offer elegant solutions to common problems, fostering code that is not only more maintainable but also easier to understand and extend. These patterns are not mere abstractions; they are the tried-and-true blueprints that have shaped the digital landscape.
As you continue your JavaScript development journey, remember that design patterns are not silver bullets to be used indiscriminately. Each pattern has its purpose and context, and understanding when and how to apply them is the true mark of a seasoned developer.
The beauty of design patterns lies in their universality. Regardless of whether you’re building a simple web application or a complex enterprise system, design patterns provide a language that transcends the specifics of the JavaScript ecosystem. They are the foundation upon which you can build reliable, scalable, and maintainable code.
As you apply these patterns in your own projects, you’ll not only enhance your coding skills but also join a tradition of craftsmanship that has been shaping the software industry for decades. So, embrace the power of design patterns in JavaScript, and let them be your guiding light in the ever-expanding universe of web development. Happy coding!
If you enjoy this content, pls consider support me taking a look in https://market.carlosrojas.dev/
Quick Reference Guide to Design Patterns in JS. was originally published in CarlosRojasDev on Medium, where people are continuing the conversation by highlighting and responding to this story.