Elegant patterns of modern JavaScript: Ice Factory

Original author: Bill Sourour
  • Transfer
We bring to your attention a translation of the next Bill Soro material, 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 start talking about it with a description of one of the JS problems that it aims to solve.

image

JavaScript class problem


It is often worthwhile to group related functions in a single object. For example, an application for an online store may have an object cartthat contains public methods addProductand removeProduct. These methods can be called using the cart.addProduct()and constructs cart.removeProduct().

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 above situation is probably perceived by you as something completely natural.

If you just started learning programming, now you are familiar with type expressions cart.addProduct(). And I suspect that the idea of ​​grouping functions represented by the methods of a single object is also clear to you now.

Let's talk about how to create an objectcart. Perhaps the first thing that comes to your mind, given the capabilities of modern JavaScript, will be accessing a 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
})

Please note that dbI use an array as a 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 this all looks good, classes in JavaScript behave very differently than you might expect. Figuratively speaking, if you are not careful when working with classes in JS, they may bite you.

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

const db = []
const cart = new ShoppingCart({db})
cart.addProduct = () => 'nope!' 
// В строке выше нет ошибки!
cart.addProduct({ 
  name: 'foo', 
  price: 9.99
}) // вывод: "nope!" Почему?

In fact, it’s even worse, since objects created using the keyword newinherit the prototype ( 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 creating the 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, remember the dynamic keyword binding thisin JavaScript. If we pass the methods of the object somewhere cart, we may lose the original link to this. This behavior is not intuitive, it can be the source of many problems.

A common nuisance that thisprogrammers are comfortable with is when assigning an object method to an event handler. Consider a method cart.emptydesigned to empty the basket:

empty () {
    this.db = []
  }

Assign this method to the event handler of a clickcertain button on a web page:


---
document
  .querySelector('#empty')
  .addEventListener(
    'click', 
    cart.empty
  )

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

At the same time, all this happens without any error messages, since thisnow it is related not to the basket, but to the button. As a result, the call cart.emptyleads to the creation of a new property with the name of the button db, and to the assignment of this property to an empty array, and not to the impact on the property of the dbobject cart.

This error belongs to the category of those that can literally drive the developer crazy, since, on the one hand, there are no error messages, and on the other hand, code that, from the point of view of common sense, looks quite working, actually doesn’t work like that, As expected.

In order to make the above code 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 believe that in this video you can find an excellent description of all this, but here is a quote from this video: «newand 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 example with a shopping cart 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” have disappeared. Namely, having analyzed this code, we can draw the following conclusions:

  • We no longer need a keyword new. Here, to create an object cart, we simply call the usual JS function.
  • We no longer need a keyword this. Now you can access dbdirectly from the methods of the object.
  • Now the object is cartimmutable. The team Object.freeze()"freezes" him. This leads to the fact that new properties cannot be added to it, and existing properties cannot be deleted or changed. In addition, its prototype cannot be changed either. Here it is worth remembering only that the team Object.freeze()performs the so-called “shallow freezing” of the object. That is, if the returned object contains an array or another object, then the command must also be applied to them Object.freeze(). 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 generated when trying to make changes to the object.

Closed 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 thanks to the closure mechanism, which you can read more about here .

About the origins of the Ice Factory pattern


Although Factory Functions have always been in JavaScript, I was seriously inspired by the code that Douglas Crockford showed in this video to develop the Ice Factory pattern . Here is a shot from where he demonstrates the creation of an object using a function that he calls a “constructor”.


Douglas Crockford shows the code that inspired me.

My version of the code, 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 advantage of the “raising” functions to place the return expression closer to the top of the code. As a result, the one who will read this code immediately, before delving into the details, will be able to see the general picture of what is happening.

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

Note that Crockford considers feature enhancement a “weakness” of JavaScript, and he probably won’t like my approach. I talked about my attitude to this in one of my previous articles , and specifically in this commentary.

Inheritance and Ice Factory


If we continue to think about creating an application for an online store, pretty soon you can 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 will likely have catalog ( Catalog) and order ( Order) objects . Moreover, these objects are likely to have some options for open methods addProductand removeProduct.

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

Since objects created using the Ice Factory template cannot be expanded, 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 an object, which is a list of goods?

Of course we can!

The Ice Factory pattern leads us to apply the ever-lasting principle cited in one of the most influential programming books - “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 by saying the following: “However, our experience shows that designers abuse inheritance. Often, design could become better and easier 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 embed an object representing a list of products into an object representing a basket:

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

Summary


When we learn about something new, especially when it comes to something complicated enough, such as an architectural approach to designing applications, we tend to expect clear rules related to what we learn about. We want to hear something like the following: "always do this and never do this."

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 also disadvantages. They consist in the fact that objects using this pattern are created more slowly than using classes, and require more memory.

For those uses of this pattern that were described above, these minuses do not matter. In particular, even though Ice Factory is slower than using classes, this pattern still works pretty fast.

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 forefront, then ordinary classes are probably better for you.

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

Despite the fact that I said above, the features of JS-classes can not always be considered disadvantages. For example, you should not refuse a library or framework just because classes are used there. Here is a good Dan Abramov material on this subject.

And finally, I have to admit that in the code snippets that I quoted above, I used a lot of architectural solutions dictated solely by my preferences and not something like an absolute truth. Here is some of them:


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

The Ice Factory design pattern, in general, boils down to using a 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?


Also popular now: