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.

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!'
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:
- We no longer need the
new
keyword. Here, to create the cart
object, we simply call the normal JS function.
- We no longer need the
this
. Now you can access the db
directly from the object's methods.
- The
cart
object is now immutable. The Object.freeze()
command freezes it. This leads to the fact that it can not add new properties, and existing properties can not be deleted or modified. In addition, its prototype cannot be changed either. Here it is worth remembering only that the Object.freeze()
command performs the so-called “small freezing” of the object. That is, if the returned object contains an array or another object, then the Object.freeze()
command must also be applied to them. In addition, if a frozen object is used outside the ES module , you must use strict mode in order to ensure that an error is issued when you try to make changes to the object.
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 meMy 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?