JavaScript, with its widespread adoption and versatility, has become a cornerstone of modern web development. As you delve deeper into JavaScript development, understanding and utilizing patterns becomes crucial. In this article, we will embark on a journey to demystify JavaScript patterns and explore how they can enhance your coding practices.
Prerequisite
To understand the concepts and techniques discussed in this article, you are expected to have an understanding of the fundamentals of JavaScript. Familiarity with concepts like variables, functions, data types, object-oriented programming, etc. is essential.
Before we move on, let's take a moment to understand the importance of JavaScript as a programming language.
JavaScript as a programming language
JavaScript, often referred to as the "language of the web," is a dynamic, high-level programming language. It is primarily used for client-side scripting in web browsers, but it has also gained traction on the server-side with the advent of Node.js. JavaScript's key features include its ability to manipulate the DOM, handle events, provide interactivity, etc. to web pages.
That being said, let's briefly discuss the importance and purpose of Patterns in JavaScript.
Importance of patterns in JavaScript development
Patterns in JavaScript serve as proven solutions to recurring problems encountered during software development. They provide structure, improve code organization, enhance maintainability, and promote reusability. By understanding and applying patterns, developers can write cleaner, more efficient code and effectively tackle complex challenges.
Purpose of understanding JavaScript patterns
Understanding JavaScript patterns goes beyond memorizing syntax or following best practices. It empowers developers to think critically about software design, choose appropriate solutions, and build scalable applications. By mastering JavaScript patterns, you gain valuable insights into the language and its ecosystem, enabling you to write robust and maintainable code.
Now that we know the importance and purpose of JavaScript Patterns, let's delve into the fundamentals of JS Design Patterns.
The Fundamentals of Design Patterns
In this section, we lay the groundwork for understanding design patterns in the context of JavaScript development.
Definition and characteristics of design patterns
Design patterns are reusable templates that encapsulate best practices for solving recurring software design problems. They offer a structured approach to designing software systems and promote modular, flexible, and maintainable code. Common characteristics of design patterns include their purpose, structure, participants, and collaborations.
Types of design patterns
Design patterns can be categorized into three main types:
Creational
Structural
Behavioral
Understanding these categories helps identify the appropriate pattern for a given problem.
- Creational Patterns
Creational patterns focus on object creation mechanisms, providing ways to instantiate objects in a flexible and controlled manner. Some commonly used creational patterns in JavaScript include:
Singleton
Factory
Constructor
Prototype
Builder
Module
Singleton Pattern
The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is useful when you want to limit the number of instances of a class and ensure a single shared instance is accessible throughout the application.
// Implementation example of the Singleton Pattern
class Singleton {
constructor() {
if (!Singleton.instance) {
// Initialize the instance
Singleton.instance = this;
}
return Singleton.instance;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // Output: true
In this example, the Singleton class has a constructor that checks if an instance of the class already exists. If an instance doesn't exist (!Singleton.instance
condition), it initializes the instance by assigning this to Singleton.instance
. This ensures that subsequent calls to the constructor will return the same instance.
When instance1 and instance2 are created using the new Singleton() syntax, both variables refer to the same instance of the Singleton class. Hence, when comparing instance1 === instance2 using the strict equality operator, it evaluates to true.
Factory Pattern
The Factory Pattern provides a way to create objects without specifying their concrete classes. It encapsulates the object creation logic in a separate factory method, allowing flexibility and decoupling between the creator and the created objects.
// Implementation example of the Factory Pattern
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
}
}
class CarFactory {
createCar(make, model) {
return new Car(make, model);
}
}
const factory = new CarFactory();
const myCar = factory.createCar("Tope", "Model 1");
In this example, a CarFactory
instance is created using new CarFactory()
, and then the createCar
method is invoked on the factory with the arguments "Tope" and "Model 1". This creates a new Car object with the make "Tope" and model "Model 1", which is assigned to the myCar
variable.
Constructor Pattern
The Constructor Pattern creates objects from a constructor function using the new
keyword. It allows you to define and initialize object properties within the constructor function.
// Implementation example of the Constructor Pattern
function Person(name, age) {
this.name = name;
this.age = age;
}
const tope = new Person("Tope", 24);
The above code defines a constructor function called Person that takes two parameters: name and age. Inside the function, the name and age values are assigned to the respective properties of the newly created object using the this keyword.
Later, a new instance of the Person object is created by invoking the Person function with the arguments "Tope" and 24. This creates a new object with the name property set to "Tope" and the age property set to 24, which is then assigned to the variable tope. The output of this code is that Tope holds an object representing a person with the name "Tope" and the age of 24.
Prototype Pattern
The Prototype pattern in JavaScript focuses on creating objects by cloning or extending existing objects as prototypes. It allows us to create new instances without explicitly defining their classes. In this pattern, objects act as prototypes for creating new objects, enabling inheritance and the sharing of properties and methods among multiple objects.
// Prototype object
const carPrototype = {
wheels: 4,
startEngine() {
console.log("Engine started.");
},
stopEngine() {
console.log("Engine stopped.");
}
};
// Create new car instance using the prototype
const car1 = Object.create(carPrototype);
car1.make = "Toyota";
car1.model = "Camry";
// Create another car instance using the same prototype
const car2 = Object.create(carPrototype);
car2.make = "Honda";
car2.model = "Accord";
car1.startEngine(); // Output: "Engine started."
car2.stopEngine(); // Output: "Engine stopped."
In this example, car instances car1 and car2 are created using a prototype object carPrototype. car1 has the make "Toyota" and model "Camry", while car2 has the make "Honda" and model "Accord". When car1.startEngine()
is called, it outputs "Engine started.", and when car2.stopEngine()
is called, it outputs "Engine stopped.". This demonstrates the utilization of a prototype object to share properties and methods among multiple instances.
Builder Pattern
In the Builder pattern, a builder class or object is responsible for constructing the final object. It provides a set of methods to configure and set the properties of the object being built. The construction process typically involves invoking these methods in a specific order to gradually build the object.
class CarBuilder {
constructor() {
this.car = new Car();
}
setMake(make) {
this.car.make = make;
return this;
}
setModel(model) {
this.car.model = model;
return this;
}
setEngine(engine) {
this.car.engine = engine;
return this;
}
setWheels(wheels) {
this.car.wheels = wheels;
return this;
}
build() {
return this.car;
}
}
class Car {
constructor() {
this.make = "";
this.model = "";
this.engine = "";
this.wheels = 0;
}
displayInfo() {
console.log(`Make: ${this.make}, Model: ${this.model}, Engine: ${this.engine}, Wheels: ${this.wheels}`);
}
}
// Usage
const carBuilder = new CarBuilder();
const car = carBuilder.setMake("Toyota").setModel("Camry").setEngine("V6").setWheels(4).build();
car.displayInfo(); // Output: Make: Toyota, Model: Camry, Engine: V6, Wheels: 4
In this example, the CarBuilder
class allows for the construction of Car objects with different properties. By calling setMake
, setModel
, setEngine
, setWheels
methods, the properties of the Car object are set. The build method finalizes the construction and returns the fully built Car object. The Car class represents a car and includes a displayInfo
method to log its details. By creating a carBuilder
instance and chaining the property-setting methods, a car object is constructed with specific make, model, engine, and wheel values. Invoking car.displayInfo()
displays the car's information.
Module Pattern
The Module Pattern encapsulates related methods and properties into a single module, providing a clean way to organize and protect the code. It allows for private and public members, enabling information hiding and preventing global namespace pollution.
const MyModule = (function() {
// Private members
let privateVariable = "I am private";
function privateMethod() {
console.log("This is a private method");
}
// Public members
return {
publicVariable: "I am public",
publicMethod() {
console.log("This is a public method");
// Accessing private members within the module
console.log(privateVariable);
privateMethod();
}
};
})();
// Usage
console.log(MyModule.publicVariable); // Output: "I am public"
MyModule.publicMethod(); // Output: "This is a public method" "I am private" "This is a private method"
In this example, the code uses an immediately invoked function expression (IIFE) to encapsulate private and public members. The module has private variables and methods, as well as public variables and methods. When accessed, the public members provide the expected output. This pattern allows for controlled access to encapsulated private members while exposing selected public members.
- Structural patterns
Structural patterns focus on organizing and composing objects to form larger structures. They facilitate the composition of objects, defining relationships between them and providing flexible ways to manipulate their structure. Some commonly used structural patterns in JavaScript include:
Decorator Pattern
Facade Pattern
Adapter
Bridge
Composite
Decorator Pattern
The Decorator Pattern allows you to add behavior or modify the existing behavior of an object dynamically. It enhances the functionality of an object by wrapping it with one or more decorators without modifying its structure.
// Implementation example of the Decorator Pattern
class Coffee {
getCost() {
return 1;
}
}
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee;
}
getCost() {
return this.coffee.getCost() + 0.5;
}
}
const myCoffee = new Coffee();
const coffeeWithMilk = new CoffeeDecorator(myCoffee);
console.log(coffeeWithMilk.getCost()); // Output: 1.5
In this example, the CoffeeDecorator
class wraps a base Coffee
object and adds additional functionality. It has a getCost
method that calculates the total cost by combining the cost of the base coffee with an additional cost of 0.5.
In the usage section, a myCoffee
instance of the Coffee
class is created. Then, a coffeeWithMilk
instance of the CoffeeDecorator
class is instantiated, passing myCoffee
as an argument. When coffeeWithMilk.getCost()
is called, it returns the total cost of the coffee with the added cost from the decorator, resulting in an output of 1.5. This example illustrates how the decorator pattern can extend the functionality of an object by dynamically adding or modifying its properties or methods.
Facade Pattern
The Facade Pattern provides a simplified interface to a complex subsystem, acting as a front-facing interface that hides the underlying implementation details. It offers a convenient way to interact with a complex system by providing a high-level interface.
// Implementation example of the Facade Pattern
class SubsystemA {
operationA() {
console.log("Subsystem A operation.");
}
}
class SubsystemB {
operationB() {
console.log("Subsystem B operation.");
}
}
class Facade {
constructor() {
this.subsystemA = new SubsystemA();
this.subsystemB = new SubsystemB();
}
operation() {
this.subsystemA.operationA();
this.subsystemB.operationB();
}
}
const facade = new Facade();
facade.operation(); // Output: "Subsystem A operation." "Subsystem B operation."
In this example, the code consists of three classes: SubsystemA
, SubsystemB
, and Facade
. The SubsystemA
and SubsystemB
classes represent independent subsystems and have their respective operationA
and operationB
methods. The Facade
class serves as a simplified interface that aggregates the functionality of the subsystems.
In the usage section, a facade
instance of the Facade
class is created. Invoking facade.operation()
triggers the execution of operationA
from SubsystemA
and operationB
from SubsystemB
. As a result, the output displays "Subsystem A operation." followed by "Subsystem B operation." This demonstrates how the Facade pattern provides a unified and simplified interface to interact with complex subsystems, abstracting their complexities and making them easier to use.
Adapter Pattern
The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate by acting as a bridge between them. It provides a way to convert the interface of one object into another interface that clients expect.
// Implementation
class LegacyPrinter {
printLegacy(text) {
console.log(`Legacy Printing: ${text}`);
}
}
// Target interface
class Printer {
print(text) {}
}
// Adapter
class PrinterAdapter extends Printer {
constructor() {
super();
this.legacyPrinter = new LegacyPrinter();
}
print(text) {
this.legacyPrinter.printLegacy(text);
}
}
// Usage
const printer = new PrinterAdapter();
printer.print("Hello, World!"); // Output: "Legacy Printing: Hello, World!"
In this code, the Adapter pattern is used to bridge the gap between the LegacyPrinter
class and a desired Printer
interface. The PrinterAdapter
extends the Printer
class and internally utilizes the LegacyPrinter
to adapt the print
method. When printer.print("Hello, World!")
is called, it effectively triggers the legacy printing functionality with the output "Legacy Printing: Hello, World!". This shows how the Adapter pattern enables the integration of incompatible components by providing a standardized interface.
Bridge Pattern
The Bridge pattern is a structural design pattern that separates the abstraction and implementation of a system, allowing it to evolve independently. It introduces a bridge between the two by using an interface or abstract class. Here's an example code snippet to illustrate the Bridge pattern:
// Example
class Shape {
constructor(color) {
this.color = color;
}
draw() {}
}
// Concrete Abstractions
class Circle extends Shape {
draw() {
console.log(`Drawing a ${this.color} circle`);
}
}
class Square extends Shape {
draw() {
console.log(`Drawing a ${this.color} square`);
}
}
// Implementor
class Color {
getColor() {}
}
// Concrete Implementors
class RedColor extends Color {
getColor() {
return "red";
}
}
class BlueColor extends Color {
getColor() {
return "blue";
}
}
// Usage
const redCircle = new Circle(new RedColor());
redCircle.draw(); // Output: "Drawing a red circle"
const blueSquare = new Square(new BlueColor());
blueSquare.draw(); // Output: "Drawing a blue square"
In this example, we have the Abstraction represented by the Shape class, which has a color property and a draw method. The Concrete Abstractions, Circle and Square, inherit from the Shape class and implement their specific draw behavior. The Implementor
is represented by the Color class, which declares the getColor
method. The Concrete Implementors
, RedColor
, and BlueColor
, inherit from the Color class and provide their respective color implementations.
In the usage section, we create instances of the Concrete Abstractions, passing the appropriate Concrete Implementor objects. This allows the Abstraction to delegate the color-related functionality to the Implementor. When we invoke the draw method, it accesses the color from the Implementor and performs the drawing operation accordingly.
Composite Pattern
The Composite pattern is a structural design pattern that allows you to treat individual objects and compositions of objects uniformly. It enables you to create hierarchical structures where each element can be treated as a single object or a collection of objects. The pattern uses a common interface to represent both individual objects (leaf nodes) and compositions (composite nodes), allowing clients to interact with them uniformly.
// Implementation
class Employee {
constructor(name) {
this.name = name;
}
print() {
console.log(`Employee: ${this.name}`);
}
}
// Composite
class Manager extends Employee {
constructor(name) {
super(name);
this.employees = [];
}
add(employee) {
this.employees.push(employee);
}
remove(employee) {
const index = this.employees.indexOf(employee);
if (index !== -1) {
this.employees.splice(index, 1);
}
}
print() {
console.log(`Manager: ${this.name}`);
for (const employee of this.employees) {
employee.print();
}
}
}
// Usage
const john = new Employee("John Doe");
const jane = new Employee("Jane Smith");
const mary = new Manager("Mary Johnson");
mary.add(john);
mary.add(jane);
const peter = new Employee("Peter Brown");
const bob = new Manager("Bob Williams");
bob.add(peter);
bob.add(mary);
bob.print();
In this example, we have the Component class Employee, which represents individual employees. The Composite class Manager extends the Employee class and can contain a collection of employees. It provides methods to add and remove employees from the collection and overrides the print method to display the manager's name and the employees under them.
In the usage section, we create a composite hierarchy where Manager objects can contain both individual employees (Employee) and other managers (Manager). We add employees to managers, constructing a hierarchical structure. Finally, we invoke the print method on the top-level manager, which recursively prints the hierarchy, showing the managers and their respective employees.
- Behavioral patterns
Behavioral patterns focus on the interaction between objects and the distribution of responsibilities. They provide solutions for communication, coordination, and collaboration among objects. The following are types of behavioral patterns.
Observer Pattern
Strategy Pattern
Command Pattern
Iterator Pattern
Mediator Pattern
Observer Pattern
The Observer Pattern establishes a one-to-many relationship between objects, where multiple observers are notified of changes in the subject's state. It enables loose coupling between objects and promotes event-driven communication.
// Implementation example of the Observer Pattern
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() {
this.observers.forEach((observer) => observer.update());
}
}
class Observer {
update() {
console.log("Observer is notified of changes.");
}
}
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers(); // Output: "Observer is notified of changes." "Observer is notified of changes."
In this example, the Subject
class represents a subject that maintains a list of observers and provides methods to add, remove, and notify observers. The Observer
class defines the behavior of an observer with its update
method. In the usage section, a subject
instance of the Subject
class is created. Two observer
instances are also created and added to the subject using the addObserver
method.
When subject.notifyObservers()
is invoked, it triggers the update
method for each observer. As a result, the output "Observer is notified of changes." is logged twice, indicating that the observers have been notified of the changes in the subject.
Strategy Pattern
The Strategy Pattern allows you to encapsulate interchangeable algorithms within separate strategy objects. It enables dynamic selection of algorithms at runtime, promoting flexibility and extensibility.
// Implementation example of the Strategy Pattern
class Context {
constructor(strategy) {
this.strategy = strategy;
}
executeStrategy() {
this.strategy.execute();
}
}
class ConcreteStrategyA {
execute() {
console.log("Strategy A is executed.");
}
}
class ConcreteStrategyB {
execute() {
console.log("Strategy B is executed.");
}
}
const contextA = new Context(new ConcreteStrategyA());
contextA.executeStrategy(); // Output: "Strategy A is executed."
const contextB = new Context(new ConcreteStrategyB());
contextB.executeStrategy(); // Output: "Strategy B is executed."
In this example, the Context
class represents a context that encapsulates different strategies, with a strategy
property and an executeStrategy
method. There are two concrete strategy classes, ConcreteStrategyA
and ConcreteStrategyB
, each with its own execute
method that outputs a specific message.
In the usage section, a contextA
instance of the Context
class is created with ConcreteStrategyA
as the strategy. Calling contextA.executeStrategy()
invokes the execute
method of ConcreteStrategyA
, resulting in the output "Strategy A is executed." Similarly, a contextB
instance is created with ConcreteStrategyB
as the strategy, and invoking contextB.executeStrategy()
triggers the execute
method of ConcreteStrategyB
, resulting in the output "Strategy B is executed." This demonstrates how the Strategy pattern allows for dynamic selection of behavior at runtime by encapsulating it in different strategy objects.
Command Pattern
The Command Pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undo operations. It decouples the sender of a request from the receiver, promoting loose coupling and flexibility.
// Implementation
class Receiver {
execute() {
console.log("Receiver executes the command.");
}
}
class Command {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
this.receiver.execute();
}
}
class Invoker {
setCommand(command) {
this.command = command;
}
executeCommand() {
this.command.execute();
}
}
const receiver = new Receiver();
const command = new Command(receiver);
const invoker = new Invoker();
invoker.setCommand(command);
invoker.executeCommand(); // Output: "Receiver executes the command."
In this example, the Receiver
class executes the command when called, and the Command
class encapsulates a command and delegates execution to the receiver. The Invoker
class sets and executes a command. In the usage section, a receiver, command, and invoker are created. The command is set for the invoker, and invoking invoker.executeCommand()
executes the command, resulting in the output "Receiver executes the command."
Iterator Pattern
The Iterator pattern is a behavioral design pattern that provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. It allows you to traverse a collection of objects in a uniform manner, regardless of the specific implementation of the collection. The pattern separates the traversal logic from the collection, promoting a clean and flexible approach to iterating over elements.
// Implementation
class Collection {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
}
createIterator() {}
}
// Concrete Aggregate
class ConcreteCollection extends Collection {
createIterator() {
return new ConcreteIterator(this);
}
}
// Iterator
class Iterator {
constructor(collection) {
this.collection = collection;
this.index = 0;
}
hasNext() {}
next() {}
}
// Concrete Iterator
class ConcreteIterator extends Iterator {
hasNext() {
return this.index < this.collection.items.length;
}
next() {
return this.collection.items[this.index++];
}
}
// Usage
const collection = new ConcreteCollection();
collection.addItem("Item 1");
collection.addItem("Item 2");
collection.addItem("Item 3");
const iterator = collection.createIterator();
while (iterator.hasNext()) {
console.log(iterator.next());
}
In this code, we have the Aggregate represented by the Collection class, which defines the interface for creating an iterator object. The Concrete Aggregate, ConcreteCollection
, extends the Collection class and provides a concrete implementation of the iterator creation.
The Iterator is represented by the Iterator class, which defines the interface for accessing and traversing elements. The Concrete Iterator, ConcreteIterator
, extends the Iterator class and provides a concrete implementation of the iteration logic. In the usage section, we create an instance of the Concrete Aggregate, ConcreteCollection
, and add items to it. We then create an iterator using the createIterator
method. By using the iterator's hasNext
and next methods, we iterate over the collection and print each item.
Mediator Pattern
The Mediator pattern simplifies object communication by introducing a mediator object that serves as a central hub for coordinating interactions between objects. It encapsulates the communication logic and provides methods for objects to register, send, and receive messages.
// Implementation
class Mediator {
constructor() {
this.colleague1 = null;
this.colleague2 = null;
}
setColleague1(colleague) {
this.colleague1 = colleague;
}
setColleague2(colleague) {
this.colleague2 = colleague;
}
notifyColleague1(message) {
this.colleague1.receive(message);
}
notifyColleague2(message) {
this.colleague2.receive(message);
}
}
class Colleague {
constructor(mediator) {
this.mediator = mediator;
}
send(message) {
// Send a message to the mediator
this.mediator.notifyColleague2(message);
}
receive(message) {
console.log(`Received message: ${message}`);
}
}
// Usage
const mediator = new Mediator();
const colleague1 = new Colleague(mediator);
const colleague2 = new Colleague(mediator);
mediator.setColleague1(colleague1);
mediator.setColleague2(colleague2);
colleague1.send("Hello Colleague 2!"); // Output: "Received message: Hello Colleague 2!"
In this example, we have a Mediator class that acts as an intermediary between two Colleague objects. The Mediator holds references to the colleagues and provides methods to send messages between them.
Each Colleague object has a reference to the mediator and can send messages by notifying the mediator. The mediator, in turn, relays the messages to the appropriate colleagues. In this case, Colleague 1 sends a message to Colleague 2, and the latter receives and logs the message.
Conclusion
We have explored a range of essential design patterns in JavaScript, encompassing creational, structural, and behavioral patterns. The creational patterns allow us to create objects in a flexible and efficient manner. Structural patterns aid in organ flexibility and scalability. Behavioral patterns enable effective communication and interaction between JavaScript objects. By leveraging these design patterns, JavaScript developers can improve code reusability, maintainability, and overall system performance. Armed with this knowledge, we can architect robust and efficient JavaScript applications that meet the demands of modern software development.
Top comments (20)
That's not just JS patterns, it's widely known Gang of Four Patterns.
en.m.wikipedia.org/wiki/Design_Pat...
If you want to know more about Software Design:
en.m.wikipedia.org/wiki/Software_d...
Comprehensive read about System design patterns:
en.m.wikipedia.org/wiki/Category:S...
There is only one downgrade in Wikipedia, the language may be complicated to understand.
I came here to say this exact same thing. These are the typical GoF patterns.
Thank you, this found me right in time. I live the way you break down reach pattern and explain the use case asking with the example. This is by far one of the most helpful articles I've read all year!
The example of Bridge Pattern is not printing the colors as expected.
I resolved this by invoking the getColor() method for each Color class implementor object:
const redColor = new RedColor().getColor();
const redCircle = new Circle(redColor);
redCircle.draw(); // Output: "Drawing a red circle"
const blueColor = new BlueColor().getColor();
const blueSquare = new Square(blueColor);
blueSquare.draw(); // Output: "Drawing a blue square"
The singleton example isnāt very idiomatic JS if you ask meā¦it seems like you were thinking within the conventions of some other language youāre experienced in rather than a natural way of doing things in JS. Two calls to new returning the same object is very very unusual behavior, thus a nasty surprise. JS programmers I know just export a const, or a getter function that returns the same value.
I also found this strange but good to know as possibility.
As already pointed out, these are GoF or general OOP patterns. What I would also like to point out, that -at least for me- this ist not how you would solve the underlying problems in JS / TS, as it's not inherently an OOP language, but a prototype-based language with first-class functions, it's unique way of what you can do with objects and the way the module-system works. For example:
Singleton Pattern
Moules are effectively singletons, so no need to use any other pattern
Generally I rarely use classes at all, instead I usually use (typed) objects. My favorite example is the Command pattern.
Command Pattern
This example also uses TypeScript to make sure a command always looks the same. You could also use it with vanilla JS and manually assert command objects always having the same fields. I also added a simple logic to implement undoing commands which in my opinion is one of the biggest real-world strengths / use-cases of this pattern.
Seems like a copy paste from other articles.
https://www.linkedin.com/advice/how-can-you-coordinate-object-interactions?utm_source=share&utm_medium=member_android&utm_campaign=share_via
Great article. I've been doing this for so long that sometimes I forget which patterns I'm using.
The Colleague class in the Mediator example does not look like it will work if
colleague2
sends a message.Your mediator would benefit from a generic send that can determine which objects receive the message:
Thanks for this post!
The title "JS Design Patterns" is misleading, it is not specific to JS. Most programming language that uses OOP can do that. You just write the example in JS.
I think it better to title as Design Patterns with JS examples..
And... you can achieve all of those without using the word CLASS in JS. š
Thats a lot of patterns to read, bookmarking lol
I'm not sure but it seems like using ChatGPT to write this article š
Why does he not need to call super() in the bridge patter example?
Thank you for this
God tier article
I found your post useful even if using of some models is not recommended in JS.
Thanks ! :)