📜 ⬆️ ⬇️

Classes and factory functions in javascript. What to choose?

There are various ways to create objects in JavaScript. In particular, we are talking about constructions using the class keyword and the so-called factory functions (Factory Function). The author of the material, the translation of which we publish today, explores and compares these two concepts in search of an answer to the question of the pros and cons of each of them.

image

Overview


The class keyword appeared in ECMAScript 2015 (ES6), with the result that we now have two competing object creation patterns. In order to compare them, I will describe the same object ( TodoModel ), using the syntax of the classes, and applying the factory function.

Here’s the TodoModel description using the class keyword:
')
 class TodoModel {   constructor(){       this.todos = [];       this.lastChange = null;   }     addToPrivateList(){      console.log("addToPrivateList");   }   add() { console.log("add"); }   reload(){} } 

Here is a description of the same object made by means of a factory function:

 function TodoModel(){   var todos = [];   var lastChange = null;         function addToPrivateList(){       console.log("addToPrivateList");   }   function add() { console.log("add"); }   function reload(){}     return Object.freeze({       add,       reload   }); } 

Consider the features of these two approaches to the creation of classes.

Encapsulation


The first feature that can be noticed when comparing classes and factory functions is that all members, fields, and methods of objects created using the class keyword are publicly available.

 var todoModel = new TodoModel(); console.log(todoModel.todos);     //[] console.log(todoModel.lastChange) //null todoModel.addToPrivateList();     //addToPrivateList 

When using factory functions, only that which we consciously discover is generally available, everything else is hidden inside the received object.

 var todoModel = TodoModel(); console.log(todoModel.todos);     //undefined console.log(todoModel.lastChange) //undefined todoModel.addToPrivateList();     //taskModel.addToPrivateList                                   is not a function 

API Immunity


After the object is created, I expect that its API will not change, that is, I expect immobility from it. However, we can easily change the implementation of the public methods of objects created with the class keyword.

 todoModel.reload = function() { console.log("a new reload"); } todoModel.reload();            //a new reload 

This problem can be solved by calling Object.freeze(TodoModel.prototype) after declaring a class, or using the decorator to “freeze” classes when it is supported.

On the other hand, the API of an object created using a factory function is immutable. Note the use of the Object.freeze() command to process the returned object, which contains only the public methods of the new object. Private data of this object can be modified, but this can be done only through these public methods.

 todoModel.reload = function() { console.log("a new reload"); } todoModel.reload();            //reload 

Keyword this


Objects created using the class keyword are subject to the long-term problem of losing context this . For example, this loses context in nested functions. This not only complicates the programming process, such behavior is also a constant source of errors.

 class TodoModel {   constructor(){       this.todos = [];   }     reload(){       setTimeout(function log() {          console.log(this.todos);    //undefined       }, 0);   } } todoModel.reload();                   //undefined 

And this is how this loses context when using the corresponding method in a DOM event:

 $("#btn").click(todoModel.reload);    //undefined 

Objects created using factory functions do not suffer from this problem, since the keyword this not used here.

 function TodoModel(){   var todos = [];         function reload(){       setTimeout(function log() {          console.log(todos);        //[]      }, 0);   } } todoModel.reload();                   //[] $("#btn").click(todoModel.reload);    //[] 

Keyword this and arrow functions


The switch functions partially solve the problems associated with the loss of this context when using classes, but at the same time they create a new problem. Namely, when using arrow functions in classes, the keyword this no longer loses context in nested functions. However, this loses context when dealing with DOM events.

I reworked the TodoModel class using pointer functions. It is worth noting that in the process of refactoring, when replacing ordinary functions with pointer ones, we lose something important for the readability of the code: the names of functions. Take a look at the following example.

 //      setTimeout(function renderTodosForReview() {     /* code */ }, 0); //       setTimeout(() => {     /* code */ }, 0); 

When using switch functions, I have to read the text of the function in order to understand exactly what it does. I would also like to read the name of the function and understand its essence, and not to read all its code. Of course, you can ensure good readability of the code when using arrow functions. For example, you can make a habit of using switch functions like this:

 var renderTodosForReview = () => {    /* code */ }; setTimeout(renderTodosForReview, 0); 

New operator


When creating objects based on classes, use the operator new . And when creating objects using factory functions, new not required. However, if the use of new improves the readability of the code, this operator can also be used with factory functions, there will be no harm.

 var todoModel= new TodoModel(); 

When using new with a factory function, the function simply returns the object created to it.

Security


Suppose that an application uses a User object to work with authorization mechanisms. I created a couple of such objects using both approaches described here.

Here is a description of a User object using the class:

 class User {   constructor(){       this.authorized = false;   }     isAuthorized(){       return this.authorized;   } } const user = new User(); 

Here is the same object described by the factory function:

 function User() {   var authorized = false;        function isAuthorized(){      return authorized;   }     return Object.freeze({       isAuthorized   }); } const user = User(); 

Objects created using the class keyword are vulnerable to attacks if the attacker has an object reference. Since all properties of all objects are public, an attacker can use other objects to gain access to the object in which he is interested.

For example, you can obtain the corresponding rights directly from the developer console, if the user variable is global. To verify this, open the example code and modify the user variable from the console.

This example was prepared using the Plunker resource. To access global variables, change the context in the console tab from top to plunkerPreviewTarget(run.plnkr.co/) .

 user.authorized = true;            //    user.isAuthorized = function() { return true; }  // API console.log(user.isAuthorized());  //true 


Modifying an object using the developer console

An object created using a factory function cannot be modified externally.

Composition and inheritance


Classes support both inheritance and object composition.

I created an example of inheritance in which the class SpecialService is a descendant of the class Service .

 class Service {   log(){} } class SpecialService extends Service {  logSomething(){ console.log("logSomething"); } } var specialService = new SpecialService(); specialService.log(); specialService.logSomething(); 

When using factory functions, inheritance is not supported, only composition can be used here. Alternatively, you can use the Object.assign() command to copy all properties from existing objects. For example , suppose we need to reuse all members of the Service object in the SpecialService object.

 function Service() {   function log(){}          return Object.freeze({       log   }); } function SpecialService(args){  var standardService = args.standardService;  function logSomething(){      console.log("logSomething");  }  return Object.freeze(Object.assign({}, standardService, {      logSomething  })); } var specialService = SpecialService({      standardService : Service()   }); specialService.log(); specialService.logSomething(); 

Factory functions promote the use of composition instead of inheritance, which gives the developer a higher level of flexibility in terms of application design.

When using classes, you can also prefer composition over inheritance; in fact, these are just architectural solutions for reusing existing behavior.

Memory


The use of classes helps save memory, as they are implemented on the basis of a system of prototypes. All methods are created only once, in the prototype, they are used by all instances of the class.

The additional cost of memory consumed by objects created using factory functions is noticeable only when thousands of similar objects are created.

Here is the page used to figure out the memory costs that are characteristic of using factory functions. Here are the results obtained in Chrome for a different number of objects with 10 and 20 methods.


Memory cost (in Chrome)

OOP objects and data structures


Before continuing the analysis of memory costs, one should distinguish between two types of objects:


Objects provide behavior and hide data.

Data structures provide data, but do not have any significant behavior.

Robert Martin, " Clean Code ".

Take a look at an example of a TodoModel object TodoModel you already know to clarify the difference between objects and data structures.

 function TodoModel(){   var todos = [];            function add() { }   function reload(){ }        return Object.freeze({       add,       reload   }); } 

The TodoModel object TodoModel responsible for storing and managing the list of todo objects. TodoModel is an OOP object, the one that provides the behavior and hides the data. The application will have only one copy of it, so when you create it using the factory function, no additional memory costs are required.

The objects stored in the todos array are data structures. There can be many such objects in the program, but these are regular JavaScript objects. We are not interested in making their methods private. Rather, we strive to ensure that all their properties and methods would be publicly available. As a result, all these objects will be built using a prototype system, so we can save memory. They can be created using a regular object literal or by Object.create() command.

UI Components


Applications can have hundreds or thousands of instances of user interface components. This is the situation in which you need to find a compromise between encapsulation and saving memory.

Components will be created in accordance with the methods adopted in the framework used. For example, in Vue object literals are used, in React - classes. Each member of the component object will be publicly available, but, thanks to the use of a prototype system, the use of such objects will save memory.

Two opposing OOP paradigms


In a broader sense, classes and factory functions demonstrate the battle of two opposing paradigms of object-oriented programming.

Class-based OOP, as applied to JavaScript, means the following:


OOP without the use of classes is as follows:


Results


The strength of classes is that they are well known to programmers who have come to JS from languages ​​based on classes. The classes in JS are the “syntactic sugar” for the prototype system. However, security problems and the use of this , leading to permanent errors due to the loss of context, put classes in second place compared to factory functions. As an exception, classes are resorted to when they are used in the framework used, for example, in React.

Factory functions are not only a tool for creating protected, encapsulated, and flexible OOP objects. This approach to creating classes also opens the way for a new, unique to JavaScript, programming paradigm.

Let me conclude this material by quoting Douglas Crockford : "I think that PLO without classes is a gift to humanity from JavaScript."

Dear readers! What and why is closer to you: classes or factory functions?

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


All Articles