📜 ⬆️ ⬇️

JavaScript Design Patterns

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'); //  Name: Mark myRevealingModule.getName(); 

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'); //  Name: Mark myRevealingModule.getName(); 

Consider the advantages of the pattern "Open Module" before the pattern "Module":


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:


Import module


Just as there are two ways to export, there are two ways to import modules. This is done using the import keyword:


â–Ť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); //  true console.log(user1 === user2); 

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); // prints true console.log(user1 === user2); 

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?

Source: https://habr.com/ru/post/427293/


All Articles