The author of the material, the translation of which we publish, says that when starting a project, writing code is not started immediately. First of all, determine the goal and boundaries of the project, then - identify those opportunities that it should possess. Already after that, either they immediately write the code, or, if we are talking about a rather complicated project, select the appropriate design patterns that form its basis. This material is about JavaScript design patterns. It is designed mainly for novice developers.

What is a design pattern?
In the field of software engineering, a design pattern is a repeatable architectural design that represents a solution to a design problem within a context that often arises. Design patterns are a summary of the experience of professional software developers. A design pattern can be considered as a kind of template according to which programs are written.
Why do we need design patterns?
Many programmers either think that design patterns are a waste of time, or they simply don’t know how to apply them correctly. However, the use of a suitable pattern can help in writing better and more understandable code, which, due to clarity, will be easier to maintain.
')
Perhaps the most important thing here is that the use of patterns gives software developers something of a vocabulary of well-known terms that are very useful, for example, when parsing someone else's code. Patterns reveal the purpose of certain fragments of the program for those who are trying to deal with the device of a project.
For example, if you use the “Decorator” pattern, it will immediately inform the new programmer who came to the project about exactly what tasks a certain code fragment solves and why it is needed. Due to this, such a programmer will be able to devote more time to practical tasks that the program solves, rather than trying to understand its internal structure.
Now, when we figured out what design patterns are and what they are for, let's move on to the patterns and to the description of their implementation using JavaScript.
Pattern "Module"
A module is an independent piece of code that can be changed without affecting another project code. The modules, in addition, allow you to avoid the phenomenon of pollution of the areas of visibility, due to the fact that they create separate areas of visibility for the variables declared in them. Modules written for one project can be reused in other projects, if their mechanisms are universal and not tied to the features of a particular project.
Modules are an integral part of any modern JavaScript application. They help maintain the purity of the code, contribute to the separation of code into meaningful fragments and help to organize it. In JavaScript, there are many ways to create modules, one of which is the Module pattern.
Unlike other programming languages, JavaScript does not have access modifiers. That is, variables cannot be declared private or public. As a result, the “Module” pattern is also used to emulate the concept of encapsulation.
This pattern uses IIFE (Immediately-Invoked Functional Expression, immediately called functional expression), closures, and function scopes to simulate this concept. For example:
const myModule = (function() { const privateVariable = 'Hello World'; function privateMethod() { console.log(privateVariable); } return { publicMethod: function() { privateMethod(); } } })(); myModule.publicMethod();
Since we are holding IIFE, the code is executed immediately and the object returned by the expression is assigned to the
myModule
constant. Due to the fact that there is a closure, the returned object has access to the functions and variables declared inside the IIFE, even after the completion of the IIFE.
As a result, the variables and functions declared inside the IIFE are hidden from the mechanisms that are in the field of visibility external to them. They turn out to be private entities of the
myModule
constant.
After this code is executed,
myModule
will look like this:
const myModule = { publicMethod: function() { privateMethod(); }};
That is, referring to this constant, you can call the public method of the object
publicMethod()
, which, in turn, will call the private method
privateMethod()
. For example:
// 'Hello World' module.publicMethod();
Open Module Pattern
The Revealing Module pattern is a slightly improved version of the Module pattern proposed by Christian Heilmann. The problem with the “Module” pattern is that we have to create public functions only to access private functions and variables.
In the pattern in question, we assign private functions to the properties of the returned object that we want to make publicly available. That is why this pattern is called the "Open Module". Consider an example:
const myRevealingModule = (function() { let privateVar = 'Peter'; const publicVar = 'Hello World'; function privateFunction() { console.log('Name: '+ privateVar); } function publicSetName(name) { privateVar = name; } function publicGetName() { privateFunction(); } return { setName: publicSetName, greeting: publicVar, getName: publicGetName }; })(); myRevealingModule.setName('Mark');
The application of this pattern simplifies the understanding of which functions and variables of the module are publicly available, which contributes to improving the readability of the code.
After executing IIFE,
myRevealingModule
looks like this:
const myRevealingModule = { setName: publicSetName, greeting: publicVar, getName: publicGetName };
We can, for example, call the
myRevealingModule.setName('Mark')
method, which is a reference to the internal
publicSetName
function. The
myRevealingModule.getName()
method refers to the internal function
publicGetName
. For example:
myRevealingModule.setName('Mark');
Consider the advantages of the pattern "Open Module" before the pattern "Module":
- The "open module" allows you to make publicly available the hidden entities of the module (and again hide them, if necessary), modifying, for each of them, only one line in the object returned after the execution of IIFE.
- The object returned does not contain a function definition. Everything to the right of its property names is defined in IIFE. This contributes to the purity of the code and simplifies its reading.
Modules in ES6
Prior to the release of the ES6 standard in JavaScript, there was no standard means for working with modules; as a result, developers had to use third-party libraries or the Module pattern to implement the appropriate mechanisms. But with the advent of ES6 in JavaScript, a standard system of modules appeared.
ES6 modules are stored in files. One file can contain only one module. Everything inside the module is private by default. Functions, variables, and classes can be made public using the
export
keyword. The code inside the module is always executed in strict mode.
â–Ť Export module
There are two ways to export a function or variable declared in a module:
- Export is done by adding the
export
keyword before declaring a function or variable. For example:
// utils.js export const greeting = 'Hello World'; export function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } export function subtract(num1, num2) { console.log('Subtract:', num1, num2); return num1 - num2; } // - function privateLog() { console.log('Private Function'); }
- The export is performed by adding the
export
keyword to the end of the code listing the names of the functions and variables to be exported. For example:
// utils.js function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } function divide(num1, num2) { console.log('Divide:', num1, num2); return num1 / num2; } // function privateLog() { console.log('Private Function'); } export {multiply, divide};
Import module
Just as there are two ways to export, there are two ways to import modules. This is done using the
import
keyword:
- Import multiple favorites. For example:
// main.js // import { sum, multiply } from './utils.js'; console.log(sum(3, 7)); console.log(multiply(3, 7));
- Import all that the module exports. For example:
// main.js // , import * as utils from './utils.js'; console.log(utils.sum(3, 7)); console.log(utils.multiply(3, 7));
â–ŤNicknames for exported and imported entities
If the names of the functions or variables exported to the code can cause a collision, they can be changed during the export or during the import.
To rename entities during export, you can do the following:
// utils.js function sum(num1, num2) { console.log('Sum:', num1, num2); return num1 + num2; } function multiply(num1, num2) { console.log('Multiply:', num1, num2); return num1 * num2; } export {sum as add, multiply};
To rename entities during import, the following construction is used:
// main.js import { add, multiply as mult } from './utils.js'; console.log(add(3, 7)); console.log(mult(3, 7));
Pattern "Singleton"
The pattern "Singleton" or "Singleton" (Singleton) is an object that can exist only in a single copy. As part of this pattern, a new instance of a certain class is created if it has not yet been created. If an instance of the class already exists, then when you try to access the constructor, a reference to the corresponding object is returned. Subsequent constructor calls will always return the same object.
In fact, what we call the pattern “Singleton” has always existed in JavaScript, but it is not called “Singleton”, but “object literal”. Consider an example:
const user = { name: 'Peter', age: 25, job: 'Teacher', greet: function() { console.log('Hello!'); } };
Since each object in JavaScript occupies its own area of ​​memory and does not share it with other objects, whenever we access the
user
variable, we get a link to the same object.
The Singleton pattern can be implemented using the constructor function. It looks like this:
let instance = null; function User(name, age) { if(instance) { return instance; } instance = this; this.name = name; this.age = age; return instance; } const user1 = new User('Peter', 25); const user2 = new User('Mark', 24);
When a constructor function is called, it first checks if an
instance
object exists. If the corresponding variable is not initialized,
this
written in the
instance
. If the variable already has an object reference, the constructor simply returns an
instance
, that is, a reference to an already existing object.
The Singleton pattern can be implemented using the Module pattern. For example:
const singleton = (function() { let instance; function User(name, age) { this.name = name; this.age = age; } return { getInstance: function(name, age) { if(!instance) { instance = new User(name, age); } return instance; } } })(); const user1 = singleton.getInstance('Peter', 24); const user2 = singleton.getInstance('Mark', 26);
Here we create a new
user
instance by calling the
singleton.getInstance()
method. If an instance of the object already exists, then this method will simply return it. If there is no such object yet, the method creates its new instance by calling the
User
constructor function.
Pattern "Factory"
The Factory pattern uses the so-called “factory methods” to create objects. You do not need to specify the classes or constructor functions that are used to create objects.
This pattern is used to create objects in cases when it is not necessary to make the logic of their creation publicly available. The “Factory” pattern can be used if you need to create different objects depending on specific conditions. For example:
class Car{ constructor(options) { this.doors = options.doors || 4; this.state = options.state || 'brand new'; this.color = options.color || 'white'; } } class Truck { constructor(options) { this.doors = options.doors || 4; this.state = options.state || 'used'; this.color = options.color || 'black'; } } class VehicleFactory { createVehicle(options) { if(options.vehicleType === 'car') { return new Car(options); } else if(options.vehicleType === 'truck') { return new Truck(options); } } }
Here the classes
Car
and
Truck
, which provide for the use of certain standard values. They are used to create
car
and
truck
objects. Also, the
VehicleFactory
class is declared here, which is used to create new objects based on the analysis of the
vehicleType
property
vehicleType
to the corresponding method of the object returned by it in the object with the
options
parameters. Here's how to work with all this:
const factory = new VehicleFactory(); const car = factory.createVehicle({ vehicleType: 'car', doors: 4, color: 'silver', state: 'Brand New' }); const truck= factory.createVehicle({ vehicleType: 'truck', doors: 2, color: 'white', state: 'used' }); // Car {doors: 4, state: "Brand New", color: "silver"} console.log(car); // Truck {doors: 2, state: "used", color: "white"} console.log(truck);
The
factory
object of class
VehicleFactory
created
VehicleFactory
. After that, you can create objects of the
Car
or
Truck
classes by calling the
factory.createVehicle()
method and passing it the
options
object with the
vehicleType
property set to the value of
car
or
truck
.
Pattern "Decorator"
The Decorator pattern (Decorator) is used to extend the functionality of objects without modifying the existing classes or constructor functions. This pattern can be used to add features to objects without modifying the code that is responsible for creating them.
Here is a simple example of using this pattern:
function Car(name) { this.name = name; // this.color = 'White'; } // , const tesla= new Car('Tesla Model 3'); // - tesla.setColor = function(color) { this.color = color; } tesla.setPrice = function(price) { this.price = price; } tesla.setColor('black'); tesla.setPrice(49000); // black console.log(tesla.color);
Consider now a practical example of the application of this pattern. Suppose the cost of cars depends on their features, on their additional functions. Without using the “Decorator” pattern, for describing these cars, we would have to create different classes for different combinations of these additional functions, each of which would have a method for finding the value of the car. For example, it might look like this:
class Car() { } class CarWithAC() { } class CarWithAutoTransmission { } class CarWithPowerLocks { } class CarWithACandPowerLocks { }
Thanks to the considered pattern, you can create a base class
Car
, which describes, say, a car in the base configuration, the cost of which is expressed in some fixed amount. After that, the standard object created on the basis of this class can be extended using decorator functions. A standard “car” processed by such a function receives new possibilities, which, moreover, affects its price. For example, this scheme can be implemented as:
class Car { constructor() { // this.cost = function() { return 20000; } } } // - function carWithAC(car) { car.hasAC = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } } // - function carWithAutoTransmission(car) { car.hasAutoTransmission = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 2000; } } // - function carWithPowerLocks(car) { car.hasPowerLocks = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } }
Here we first create the base class
Car
, used to create objects representing standard cars. Then we create several decorator functions that allow us to extend the objects of the base class
Car
additional properties. These functions take the corresponding objects as parameters. After that, we add a new property to the object, indicating what new feature the car will be equipped with, and redefine the
cost
function of the object, which now returns the new value of the car. As a result, in order to “equip” a standard-configuration car with something new, we can use the following design:
const car = new Car(); console.log(car.cost()); carWithAC(car); carWithAutoTransmission(car); carWithPowerLocks(car);
After that, you can find out the value of the car in an improved configuration:
// console.log(car.cost());
Results
In this material, we have dismantled several design patterns used in JavaScript, but in fact, there are still many patterns beyond our conversation that can be used to solve a wide range of tasks.
While knowledge of various design patterns is important for a programmer, their appropriate use is equally important. Knowing about the patterns and the scope of their application, the programmer, analyzing the task in front of him, can understand which pattern can help solve it.
Dear readers! What design patterns do you use most often?
