📜 ⬆️ ⬇️

Elegant patterns of modern javascript: Ice Factory

We bring to your attention the translation of the next material by Bill Soro, which is dedicated to design patterns in JavaScript. Last time we talked about the RORO pattern, and today our theme will be the Ice Factory pattern. In a nutshell, this template is a function that returns a “frozen” object. This is a very important and powerful pattern, and we will begin to talk about it with a description of one of the JS problems, which he is addressing.

image

Javascript class problem


Related functions often make sense to group in a single object. For example, in an online store application, there may be a cart object that contains the public methods addProduct and removeProduct . These methods can be invoked using the cart.addProduct() and cart.removeProduct() constructs.

If you came to JavaScript development from languages ​​like Java or C #, where classes are at the forefront, where everything is focused on objects, then the situation described above is likely to be perceived by you as something quite natural.
')
If you are just starting to learn programming, then now you are familiar with expressions like cart.addProduct() . And I suspect that the idea of ​​grouping functions represented by methods of a single object is also clear to you now.

Talk about how to create a cart object. Perhaps the first thing that comes to your mind, given the possibilities of modern JavaScript, will be an appeal to the keyword class . For example, it might look like this:

 // ShoppingCart.js export default class ShoppingCart { constructor({db}) {   this.db = db } addProduct (product) {   this.db.push(product) } empty () {   this.db = [] } get products () {   return Object     .freeze(this.db) } removeProduct (id) {   //   } //   } // someOtherModule.js const db = [] const cart = new ShoppingCart({db}) cart.addProduct({ name: 'foo', price: 9.99 }) 

Notice that I use an array as the db parameter. This is done to simplify the example. In real code, such a variable will be represented by something like a Model or Repo object that interacts with a real database.

Unfortunately, even though all this looks good, classes in JavaScript behave quite differently than you might expect. Figuratively speaking, if you do not take care when working with classes in JS, they can bite you.

For example, objects created using the new keyword are mutable. This means that you, for example, can override their methods:

 const db = [] const cart = new ShoppingCart({db}) cart.addProduct = () => 'nope!' //     ! cart.addProduct({ name: 'foo', price: 9.99 }) // : "nope!" ? 

In fact, everything is even worse, since the objects created with the new keyword inherit the prototype of the class that was used to create them. Therefore, changes in the prototype of this class will affect all objects created on the basis of this class, and even if these changes are made after the creation of objects.

Here is an example:

 const cart = new ShoppingCart({db: []}) const other = new ShoppingCart({db: []}) ShoppingCart.prototype .addProduct = () => 'nope!' //     ! cart.addProduct({ name: 'foo', price: 9.99 }) // : "nope!" other.addProduct({ name: 'bar', price: 8.88 }) // : "nope!" 

Next, recall the dynamic binding of the this in JavaScript. If we pass somewhere the methods of the cart object, we may lose the original link to this . Such behavior is not intuitive, it can become a source of many problems.

The usual annoyance that this suits programmers is manifested when the method of an object is assigned to an event handler. Consider the cart.empty method for cleaning the basket:

 empty () {   this.db = [] } 

Let us assign this method to the click event handler of a certain button on a web page:

 <button id="empty"> Empty cart </button> --- document .querySelector('#empty') .addEventListener(   'click',   cart.empty ) 

If the user clicks on this button, then nothing will change. His cart, represented by the cart object, will remain full.

In this case, all this happens without any error messages, since this now related not to the basket, but to the button. As a result, calling cart.empty results in the creation of a new property for the button with the name db , and assigning an empty array to this property, and not affecting the db property of the cart object.

This error belongs to the category of those who are able to literally drive the developer’s mind, because, on the one hand, there are no error messages, and on the other, code that looks quite working from the standpoint of common sense does not actually work As expected.

In order to force the above code to do what we expect from it, you need to do this:

 document .querySelector("#empty") .addEventListener(   "click",   () => cart.empty() ) 

Or so:

 document .querySelector("#empty") .addEventListener(   "click",   cart.empty.bind(cart) ) 

I suppose in this video you can find an excellent description of all this, but the quote from this video: «new and this [in JavaScript] are illogical, strange, mysterious traps."

Ice Factory Pattern as a Solution to JS Class Problems


As already mentioned, the Ice Factory pattern is a function that creates and returns “frozen” objects. When using this design pattern, our shopping cart example will look like this:

 // makeShoppingCart.js export default function makeShoppingCart({ db }) { return Object.freeze({   addProduct,   empty,   getProducts,   removeProduct,   //  }) function addProduct (product) {   db.push(product) } function empty () {   db = [] } function getProducts () {   return Object     .freeze(db) } function removeProduct (id) {   //   } //   } // someOtherModule.js const db = [] const cart = makeShoppingCart({ db }) cart.addProduct({ name: 'foo', price: 9.99 }) 

Please note that our "strange, mysterious traps" disappeared. Namely, after analyzing this code, we can draw the following conclusions:


Private properties and methods


Another advantage of the Ice Factory template is that objects created with it can have private properties and methods. Consider an example:

 function makeThing(spec) { const secret = 'shhh!' return Object.freeze({   doStuff }) function doStuff () {   //      spec,   //  secret } } //  secret  const thing = makeThing() thing.secret // undefined 

This is possible due to the mechanism of closures, details of which can be here .

About the origins of the Ice Factory pattern


Although the factory functions (Factory Functions) were always in JavaScript, the development of the Ice Factory pattern was seriously inspired by the code that Douglas Crocford showed in this video. Here's a shot from where he demonstrates how to create an object using a function that he calls a “constructor”.


Douglas Crockford shows the code that inspired me

My version of the code, which is a variation of what Crockford showed, looks like this:

 function makeSomething({ member }) { const { other } = makeSomethingElse() return Object.freeze({   other,   method }) function method () {   // ,   "member" } } 

I took the opportunity to “raise” functions in order to put the return expression closer to the top of the code. As a result, the one who will read this code, immediately, before he gets into the details, will be able to see the overall picture of what is happening.

In addition, I used the spec parameter destructuring. I also gave the name to this template, calling it Ice Factory. I believe that this way it will be easier to remember and more difficult to confuse with the constructor function from the JS-classes. But, in general, my pattern and constructor are the same.
Therefore, if we are talking about the authorship of this pattern, then it belongs to Douglas Crockford.

Please note that Crockford considers elevating functions to be the “weak side” of JavaScript, and he probably will not like my approach. I spoke about my attitude to this in one of my previous articles , and specifically in this comment.

Inheritance and Ice Factory


If you continue thinking about creating an application for an online store, you can quite soon understand that the concept of adding and removing products when working with a basket appears again and again in different places.

Along with the shopping cart, we are likely to have Catalog and Order objects. In this case, these objects will most likely have some variants of the open methods addProduct and removeProduct .

We know that duplication of code is bad, so we will eventually come to create something like the productList object, which is a list of products from which we will inherit the basket, catalog and order objects.

Since objects created using the Ice Factory pattern cannot be extended, they cannot be inherited from other objects. Given this, what do we do with code duplication? Can we get some benefit from the use of the object, which is a list of goods?

Of course we can!

The Ice Factory pattern leads us to the application of the ever-current principle, cited in one of the most influential books on programming - “Object-oriented programming techniques. Design patterns. This principle: “Prefer composition to class inheritance”.

The authors of this book, known as the “Gang of Four”, continue, saying: “However, our experience shows that designers are abusing inheritance. Often the design could become better and simpler if the author relied more on the composition of objects. ”

So, here is our list of products:

 function makeProductList({ productDb }) { return Object.freeze({   addProduct,   empty,   getProducts,   removeProduct,   //  )} //   // addProduct   ... }   : function makeShoppingCart({  addProduct,  empty,  getProducts,  removeProduct,  //  }) {   return Object.freeze({     addProduct,     empty,     getProducts,     removeProduct,     someOtherMethod,    //    )} function someOtherMethod () {   //  } } 

Now we can simply inject the object representing the list of products into the object representing the basket:

 const productDb = [] const productList = makeProductList({ productDb }) const cart = makeShoppingCart(productList) 

Results


When we learn about something new, especially if we are talking about something quite complicated, such as an architectural method of designing applications, we tend to expect clear rules related to what we have learned. We want to hear something like the following: "always do it and never do it like that."

The longer I rotate in the software development environment, the better I understand that there are no such concepts as "always" and "never." A programmer always has a choice dictated by a combination of strengths and weaknesses of a particular technique applied to a specific situation.

Above, we talked about the strengths of the Ice Factory pattern. But he has his drawbacks. They consist in the fact that objects using this pattern are created more slowly than using classes, and require more memory.

For those options for using this pattern that were described above, these minuses do not matter. In particular, even though the Ice Factory is slower than the use of classes, this pattern still works quite quickly.

If you need to create hundreds of thousands of objects, so to speak, in one sitting, or you are in a situation where performance and memory consumption are in the foreground, then you will probably be better off with ordinary classes.

The main thing - do not forget to profile the application and do not strive for premature optimization. Creating objects is rarely a bottleneck.

Despite the fact that I said above, the features of the JS-classes can not always be considered weaknesses. For example, one should not abandon a library or framework only because classes are used there. Here is Dan Abramov 's good material on this topic.

And finally, I have to admit that in those code fragments that I cited above, I used many architectural solutions dictated solely by my preferences and not being something like absolute truth. Here are some of them:


You can use other approaches to code style, and this is completely normal . Style is not a pattern.

The Ice Factory design pattern, in general, comes down to using the function to create and return “frozen” objects. And how exactly to write such a function, you can decide for yourself.

Dear readers! Do you use something like the Ice Factory pattern in your projects?

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


All Articles