DEV Community

Bill Sourour
Bill Sourour

Posted on • Edited on

Elegant patterns in modern JavaScript: Ice Factory


Photo by Demi DeHerrera on Unsplash

I’ve been working with JavaScript on and off since the late nineties. I didn’t really like it at first, but after the introduction of ES2015 (aka ES6), I began to appreciate JavaScript as an outstanding, dynamic programming language with enormous, expressive power.

Over time, I’ve adopted several coding patterns that have lead to cleaner, more testable, more expressive code. Now, I am sharing these patterns with you.

I wrote about the first pattern — “RORO” — here. Don’t worry if you haven’t read it, you can read these in any order.

Today, I’d like to introduce you to the “Ice Factory” pattern.

An Ice Factory is just a function that creates and returns a frozen object. We’ll unpack that statement in a moment, but first let’s explore why this pattern is so powerful.

JavaScript classes are not so classy

It often makes sense to group related functions into a single object. For example, in an e-commerce app, we might have a cart object that exposes an addProduct function and a removeProduct function. We could then invoke these functions with cart.addProduct() and cart.removeProduct().

If you come from a Class-centric, object oriented, programming language like Java or C#, this probably feels quite natural.

If you’re new to programming — now that you’ve seen a statement like cart.addProduct(). I suspect the idea of grouping together functions under a single object is looking pretty good.

So how would we create this nice little cart object? Your first instinct with modern JavaScript might be to use a class. Something like:

// 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) {
    // remove a product 
  }

// other methods

}

// someOtherModule.js

const db = [] 
const cart = new ShoppingCart({db})
cart.addProduct({ 
  name: 'foo', 
  price: 9.99
})
Enter fullscreen mode Exit fullscreen mode

Note : I’m using an Array for the db parameter for simplicity’s sake. In real code this would be something like a Model or Repo that interacts with an actual database.

Unfortunately — even though this looks nice — classes in JavaScript behave quite differently from what you might expect.

JavaScript Classes will bite you if you’re not careful.

For example, objects created using the new keyword are mutable. So, you can actually re-assign a method:

const db = []
const cart = new ShoppingCart({db})

cart.addProduct = () => 'nope!' 
// No Error on the line above!

cart.addProduct({ 
  name: 'foo', 
  price: 9.99
}) // output: "nope!" FTW?
Enter fullscreen mode Exit fullscreen mode

Even worse, objects created using the new keyword inherit the prototype of the class that was used to create them. So, changes to a class’ prototype affect all objects created from that class — even if a change is made after the object was created!

Look at this:

const cart = new ShoppingCart({db: []})
const other = new ShoppingCart({db: []})

ShoppingCart.prototype.addProduct = () => ‘nope!’
// No Error on the line above!

cart.addProduct({ 
  name: 'foo', 
  price: 9.99
}) // output: "nope!"

other.addProduct({ 
  name: 'bar', 
  price: 8.88
}) // output: "nope!"
Enter fullscreen mode Exit fullscreen mode

Then there's the fact that this In JavaScript is dynamically bound. So, if we pass around the methods of our cart object, we can lose the reference to this. That’s very counter-intuitive and it can get us into a lot of trouble.

A common trap is assigning an instance method to an event handler.

Consider our cart.empty method.

empty () {
    this.db = []
  }
Enter fullscreen mode Exit fullscreen mode

If we assign this method directly to the click event of a button on our web page…

<button id="empty">
  Empty cart
</button>

---

document
  .querySelector('#empty')
  .addEventListener(
    'click', 
    cart.empty
  )
Enter fullscreen mode Exit fullscreen mode

… when users click the empty button, their cart will remain full.

It fails silently because this will now refer to the button instead of the cart. So, our cart.empty method ends up assigning a new property to our button called db and setting that property to [] instead of affecting the cart object’s db.

This is the kind of bug that will drive you crazy because there is no error in the console and your common sense will tell you that it should work, but it doesn’t.

To make it work we have to do:

document
  .querySelector("#empty")
  .addEventListener(
    "click", 
    () => cart.empty()
  )
Enter fullscreen mode Exit fullscreen mode

Or:

document
  .querySelector("#empty")
  .addEventListener(
    "click", 
    cart.empty.bind(cart)
  )
Enter fullscreen mode Exit fullscreen mode

I think Mattias Petter Johansson said it best:

new and this [in JavaScript] are some kind of unintuitive, weird, cloud rainbow trap.”

Ice Factory to the rescue

As I said earlier, an Ice Factory is just a function that creates and returns a frozen object. With an Ice Factory our shopping cart example looks like this:

// makeShoppingCart.js

export default function makeShoppingCart({
  db
}) {
  return Object.freeze({
    addProduct,
    empty,
    getProducts,
    removeProduct,
    // others
  })

function addProduct (product) {
    db.push(product)
  }

  function empty () {
    db = []
  }

function getProducts () {
    return Object
      .freeze(db)
  }

function removeProduct (id) {
    // remove a product
  }

// other functions
}

// someOtherModule.js

const db = []
const cart = makeShoppingCart({ db })
cart.addProduct({ 
  name: 'foo', 
  price: 9.99
})
Enter fullscreen mode Exit fullscreen mode

Notice our “weird, cloud rainbow traps” are gone:

  • We no longer need new . We just invoke a plain old JavaScript function to create our cart object.
  • We no longer need this . We can access the db object directly from our member functions.
  • Our cart object is completely immutable. Object.freeze() freezes the cart object so that new properties can’t be added to it, existing properties can’t be removed or changed, and the prototype can’t be changed either. Just remember that Object.freeze() is shallow , so if the object we return contains an array or another object we must make sure to Object.freeze() them as well. Also, if you’re using a frozen object outside of an ES Module, you need to be in strict mode to make sure that re-assignments cause an error rather than just failing silently.

A little privacy please

Another advantage of Ice Factories is that they can have private members. For example:

function makeThing(spec) {
  const secret = 'shhh!'

return Object.freeze({
    doStuff
  })

function doStuff () {
    // We can use both spec
    // and secret in here 
  }
}

// secret is not accessible out here

const thing = makeThing()
thing.secret // undefined
Enter fullscreen mode Exit fullscreen mode

This is made possible because of Closures in JavaScript, which you can read more about on MDN.

A little acknowledgement please

Although Factory Functions have been around JavaScript forever, the Ice Factory pattern was heavily inspired by some code that Douglas Crockford showed in this video.

Here’s Crockford demonstrating object creation with a function he calls “constructor”:


Douglas Crockford demonstrating the code that inspired me.

My Ice Factory version of the Crockford example above would look like this:

function makeSomething({ member }) {
  const { other } = makeSomethingElse() 

  return Object.freeze({ 
    other,
    method
  })

function method () {
    // code that uses "member"
  }
}
Enter fullscreen mode Exit fullscreen mode

I took advantage of function hoisting to put my return statement near the top, so that readers would have a nice little summary of what’s going on before diving into the details.

I also used destructuring on the spec parameter. And I renamed the pattern to “Ice Factory” so that it’s more memorable and less easily confused with the constructor function from a JavaScript class. But it’s basically the same thing.

So, credit where credit is due, thank you Mr. Crockford.

Note: It’s probably worth mentioning that Crockford considers function “hoisting” a “bad part” of JavaScript and would likely consider my version heresy. I discussed my feelings on this in a previous article and more specifically, this comment.

What about inheritance?

If we tick along building out our little e-commerce app, we might soon realize that the concept of adding and removing products keeps cropping up again and again all over the place.

Along with our Shopping Cart, we probably have a Catalog object and an Order object. And all of these probably expose some version of addProduct and removeProduct.

We know that duplication is bad, so we’ll eventually be tempted to create something like a Product List object that our cart, catalog, and order can all inherit from.

But rather than extending our objects by inheriting a Product List, we can instead adopt the timeless principle offered in one of the most influential programming books ever written:

“Favor object composition over class inheritance.” 

– Design Patterns: Elements of Reusable Object-Oriented Software.

In fact, the authors of that book — colloquially known as “The Gang of Four” — go on to say:

“…our experience is that designers overuse inheritance as a reuse technique, and designs are often made more reusable (and simpler) by depending more on object composition.”

So, here’s our product list:

function makeProductList({ productDb }) {
  return Object.freeze({
    addProduct,
    empty,
    getProducts,
    removeProduct,
    // others
  )}
  // definitions for 
  // addProduct, etc...
}
Enter fullscreen mode Exit fullscreen mode

And here’s our shopping cart:

function makeShoppingCart(productList) {

return Object.freeze({
      items: productList,
      someCartSpecificMethod,
      // ...
    )}

function someCartSpecificMethod () {
    // code 
  }
}
Enter fullscreen mode Exit fullscreen mode

And now we can just inject our Product List into our Shopping Cart, like this:

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

const cart = makeShoppingCart(productList)
Enter fullscreen mode Exit fullscreen mode

And use the Product List via the items property. Like:

cart.items.addProduct()
Enter fullscreen mode Exit fullscreen mode

It may be tempting to subsume the entire Product List by incorporating its methods directly into the shopping cart object, like so:

function makeShoppingCart({ 
   addProduct,
   empty,
   getProducts,
   removeProduct,
   ...others
  }) {

return Object.freeze({
      addProduct,
      empty,
      getProducts,
      removeProduct,
      someOtherMethod,
      ...others
    )}

function someOtherMethod () {
    // code 
  }
}
Enter fullscreen mode Exit fullscreen mode

In fact, in an earlier version of this article, I did just that. But then it was pointed out to me that this is a bit dangerous (as explained here). So, we're better off sticking with proper object composition.

Awesome. I’m Sold!


Careful

Whenever we’re learning something new, especially something as complex as software architecture and design, we tend to want hard and fast rules. We want to hear thing like “always do this” and “ never do that.”

The longer I spend working with this stuff, the more I realize that there’s no such thing as always and never. It’s about choices and trade-offs.

Making objects with an Ice Factory is slower and takes up more memory than using a class.

In the types of use case I’ve described, this won’t matter. Even though they are slower than classes, Ice Factories are still quite fast.

If you find yourself needing to create hundreds of thousands of objects in one shot, or if you’re in a situation where memory and processing power is at an extreme premium you might need a class instead.

Just remember, profile your app first and don’t prematurely optimize. Most of the time, object creation is not going to be the bottleneck.

Despite my earlier rant, Classes are not always terrible. You shouldn’t throw out a framework or library just because it uses classes. In fact, Dan Abramov wrote pretty eloquently about this in his article, How to use Classes and Sleep at Night.

Finally, I need to acknowledge that I’ve made a bunch of opinionated style choices in the code samples I’ve presented to you:

You may make different style choices, and that’s okay! The style is not the pattern.

The Ice Factory pattern is just: use a function to create and return a frozen object. Exactly how you write that function is up to you.


If you’ve found this article useful help me spread the word with some hearts and unicorns! And if you'd like to learn more stuff like this, please sign up for my Dev Mastery newsletter below. Thanks!

Dev Mastery Newsletter Sign Up

I keep your info private and I never SPAM.

Top comments (16)

Collapse
 
michaeljota profile image
Michael De Abreu • Edited

There is a series of books you (and anyone actually) should read about JS, to proper understand JS. For example, this is already a Pattern in that book, without the object.froze. You should consider as well understand what does froze, and what it does not.

I also start into programming with C++, and Java, and when I started with JS I wanted JS to behave like a proper language. Having more than 3 years working with JS the one think I can tell you is, hug JS. It have it weirdness, but it will surprise you if you let him.

If you want some kind of static type check, I strongly suggest you to use Typescript. But also, you will need a proper level of understanding about what Typescript does, because it will compile to JS at the end.

The books are You don't know JS. I think they cover most of the design patterns used in JS.

Collapse
 
billsourour profile image
Bill Sourour • Edited

Thanks for the feedback.

YDKJS is a great series of books which I have read and would recommend for sure. The author has generously made the entire thing available for free on github.

Hopefully, it comes through in this article (and its predecessor) that I am not claiming to have "invented" these patterns.

I give credit here to Crockford on the Object.freeze formulation but, of course, the idea of a Factory goes back to the original Gang of Four patterns and the idea of a Factory Function has been prominent in JavaScript programming for as long as I can remember. We are all standing on the shoulders of giants.

As for static type checking and Typescript. While it can be helpful at design time, it's completely absent at runtime because – as you pointed out – it just transpiles into JavaScript. So, it doesn't absolve you from having to code defensively for runtime input.

There is also – as I think you eluded to – a great deal of misunderstanding about what TypeScript is and how it works. TypeScript does not give you static types in JavaScript because JavaScript is not capable of static typing. So, TypeScript uses structural types which is just a formalization of duck typing.

Erirc Elliot's article on TypeScript is worth a gander for an interesting discussion of the cost/benefit of TypeScript and similar tools.

Collapse
 
xowap profile image
Rémy 🤖

The only issue with this pattern though is unit testing, private members are just completely private which can be a little bit annoying for unit testing (versus other languages where private members can be accessed through special means)

Collapse
 
billsourour profile image
Bill Sourour • Edited

In general, we should not be unit testing private members regardless of the programming language.

If we find ourselves needing to access a private member during a unit test, it is likely because the private member is pointing to something we want to mock. In that case, we should be injecting it on object creation so that it can easily be substituted when testing (consider the db parameter from the sample code in this article, for example).

As a side note, if we find ourselves doing a lot of mocking and dependency injection during unit testing, it is likely because we have not properly segregated our integration logic from our business logic. But that could be the subject of a whole other article.

Collapse
 
xowap profile image
Rémy 🤖

I've been using the Ice Factory pattern for years and even taught it to hundreds students so I quite like it (at least in ES5).

However, I've taken the example of testing but there is other ones. Suppose that there's an object Foo with an internal state bar which is a variable scoped to the constructor. For some reason, you don't want to expose bar to the world, yet you want to have a Foo.hasSameState(otherFoo) method. Since you can't access the private member, you're kinda screwed.

In short, I don't really recommend to create private members using this pattern or if you do you need to be really careful because you might very easily end up refactoring code.

Thread Thread
 
billsourour profile image
Bill Sourour

Thanks for the clarification.

If I understand correctly, what you're getting at is that – unlike some other languages – JavaScript doesn't yet support the notion of semi-private members (e.g. "protected" in Java or "friend" in C++). So, using a private member when what we really need is a semi-private member, will get us into trouble.

I definitely agree with that.

Thread Thread
 
xowap profile image
Rémy 🤖

Yes exactly :)

Collapse
 
erebos-manannan profile image
Erebos Manannán

I don't get this fear people have about their code being magically changed via prototypes, or by someone overriding the method.

Why is the acceptable solution not simply: "don't"?

Do you just accept anyone to edit your codebase as they wish and release everything to production without looking? Of course not.

On the other hand, overriding methods and magically changing your code can be super helpful in e.g. unit testing - e.g. make an internal method return a static response to trigger a special case that you might want to test.

So to me this stuff always smells a bit, and sounds like you're trying super hard to make your code more "safe" for literally 0 gains, while shooting yourself in the foot.

E.g. in Python convention is pretty much all that's needed - prefix your "private" methods with _ and people know they shouldn't be calling them if they don't know what they're doing. If they DO know what they're doing better than you did when you wrote the code (nobody is perfect, so this is bound to happen every now and then), they're going to be super annoyed if you went through massive amounts of effort to block them from doing so in your paranoia.

There is basically one case where I would consider an approach like that - when providing libraries to 3rd parties. Even then I'd have to have a pretty good justification beyond just "it's a library" to go through the effort.

Collapse
 
billsourour profile image
Bill Sourour

Thanks for taking the time to read and respond.

In my experience, when it comes to a codebase that is intended to last and to grow; to paraphrase Murphy's law, whatever can happen will happen. This tends to be true regardless of what standards, conventions, or policies are put in place.

I would also argue that overriding methods and magically changing behaviour is an anti-pattern. When it comes to unit testing, if we need to change the behaviour of some dependency we should use a mock. If there is no way to inject the mock, we probably have a design problem.

I also don't see how making objects less mutable provides "0 gains". In fact, I see our entire industry moving toward immutability as a virtue because the greatest source of bugs is unexpected and uncontrolled change.

I'm also not sure I understand what you mean by:

people know they shouldn't be calling them if they don't know what they're doing. If they DO know what they're doing better than you did when you wrote the code (nobody is perfect, so this is bound to happen every now and then), they're going to be super annoyed if you went through massive amounts of effort to block them from doing so in your paranoia.

If an object's source needs to change, then it is the authors' responsibility to change it (and by author I mean the team responsible, not necessarily the exact individual who originally wore the code). A developer who is consuming an object and does not have access to, or the ability to, alter the source of that object should never be altering its behaviour; that's just a recipe for disaster.

There are many patterns for extending an object's behaviour that do not require changing the object itself.

Collapse
 
erebos-manannan profile image
Erebos Manannán

I would also argue that overriding methods and magically changing behaviour is an anti-pattern.

Patterns and anti-patterns are mostly fads anyway, there are places for everything. The most important thing is what makes you efficient as a developer and your code better.

If your code becomes clearer, better, and you use less time by calling a "non-public" method, or by replacing a method in a class or similar, you should absolutely do so. I'll even go through a bit of effort with reflection etc. to make it possible to implement an easy and clear solution to the problem.

In fact, I see our entire industry moving toward immutability as a virtue

Well, this generally speaking is for data, and more specifically application state. Not your application code, libraries, and so on.

A developer who is consuming an object and does not have access to, or the ability to, alter the source of that object should never be altering its behaviour; that's just a recipe for disaster.

To me that sounds like you're the kind of person who likes languages with the private keyword. I don't. I hate languages with the private keyword (or really even protected).

I've found so many examples of libraries, frameworks, and just application code where someone used protected or private and that meant I had to write a massive hack, or copy & paste massive amounts of code to create a replacement, instead of just doing the thing that the developer initially didn't think was necessary.

One of the most common examples is just having a private cache, that you can't clear when you, as the user of the library/framework/code in general, know better than the original developer.

I've lost count of the amount of times I've cursed the developers who used private seem to have thought "I know better than anyone else who will ever use this code, so I'm going to stop them from doing these things".

I've similarly lost count of how many times I've seen a Python library use _ prefix on things that I actually needed so I can call them and I've thought "thanks for making that possible".

If an object's source needs to change, then it is the authors' responsibility to change it (and by author I mean the team responsible, not necessarily the exact individual who originally wore the code).

It's not quite often quite so easy. Some library published on GitHub, PyPI, NPM, etc. is not often quite as easy for you to patch (the developer just started a 3 year walkabout, but the library is used somewhere by your framework of choice, etc.) as it would be for you to patch the code if it's been properly written - with clear hints as to "if you touch these things you might break something", and for that the _ prefix to the name tends to be enough.

When it's your own code it's usually just a matter of a simple refactor to make a private method non-private to call it, extending to change the behavior, etc. .. your own code is rarely the problem, it's the other people who think they're better than anyone else ever can be and lock down their code as much as they can.

Thread Thread
 
billsourour profile image
Bill Sourour

Thanks for taking the time to reply.

It's always instructive to read a different point of view. I think we may just have to agree to disagree on pretty much all of these points.

The behaviour you seem to be describing is exactly the sort of thing that I have seen lead to serious problems in many codebases over the years. Having said that, I can appreciate that my experience is certainly not the be-all and end-all when it comes to coding, and I have been around long enough to know that there is no such thing as never and always when it comes to coding and design, it's just a matter of trade-offs.

Thanks again for your insights.

Collapse
 
eiquu0jet profile image
eiQuu0jet

Unfortunately Mattias Petter Johansson got composition completely, entirely wrong, and since you learned from him, you got composition wrong too. What he mistook for composition is actually multiple inheritance.

Collapse
 
billsourour profile image
Bill Sourour

I didn't learn about composition or inheritance from MPJ, he just provided a fitting quote about new and this that I incorporated.

Having said that, the thread you linked to makes a valid point; by destructuring the ProductList into the ShoppingCart and subsuming its methods we do, in a way, "inherit" them rather than simply make them a part of our composition. And this does lead to some of the problems we see with inheritance in general (especially, the "fragile base class" problem).

I will update the post and sample code to better reflect this. Thank you for your input.

Collapse
 
nickwalt profile image
nick walton

I really like this way of doing things (the Ice Factory Function). Thanks for writing a great article.

There seems to be a fundamental difference between the OOP way of reusing methods and properties from one object by calling them in another (object.method), and the FP way of just passing data from one function to another via arguments.

Does FP have objects with methods and properties? Could the Ice Factory Function be considered a "functionalised" OOP pattern?

I'm trying to understand how these two paradigms can be mixed together in JavaScript. What would the architecture of the application look like?

Also, the idea that an object can have properties seems to me to potentially inhibit composition because there is a tight coupling between object and data (or state).

Varun Vachar demonstrated stateless UI objects being passed state by another object. This loose coupling and the way he did that feels nicer to me than putting the state in the UI object.

Thread Thread
 
billsourour profile image
Bill Sourour • Edited

Hi Nick.

Thanks for the feedback and the question.

The Ice Factory pattern takes a decidedly OOP view of the world. It just sidesteps some of the drawbacks of inheritance and a lot of the quirkiness around JavaScript's class syntax.

It's easy to feel like anything that doesn't have the word "class" in it must be something other than OOP, but as Dave Thomas once said:

"Design with Objects, not Classes... It's OO Programming, not CO [Class-Oriented] Programming".

Having said that, FP is great. There is a lot we can learn from FP, and JavaScript is a fascinating language in that it gives us both Objects and Functions as "first class" citizens of the language, so we can take advantage of both paradigms and match the approach to the problem we are trying to solve.

In FP, we separate the data from the code that transforms the data. In our shopping cart example, the data is a list of items and the transformations include adding and removing items from the list.

An FP-style shopping cart could look something like this:

// shopping-cart-fp.js

import freezeMap from './utils/freeze-map.js';

export function addProductToCart(product, cart) {
    const newItem = Object.assign({}, product);
    newItem.qty = newItem.qty || 1;

    if (cart.has(newItem.id)) {
        newItem.qty = cart.get(newItem.id).qty + 1;
    }

    const newCart = new Map(cart);
    newCart.set(newItem.id, newItem);

    return freezeMap(newCart);
}

export function removeProductFromCart(productId, cart) {
    const newCart = new Map(cart);

    if (newCart.has(productId)) {
        const product = Object.assign({}, newCart.get(productId));

        if (product.qty < 2) {
            newCart.delete(productId);
        } else {
            --product.qty;
            newCart.set(productId, product);
        }
    }
    return freezeMap(newCart);
}

I'm using a Map here – which can't be frozen – so we need a little utility function to freeze it to ensures that no one messes with our cart data. Like so:

// freeze-map.js
export default function freezeMap(map) {
    map.set = map.clear = map.delete = () => {
        throw new Error('Attempted to change a frozen Map object.');
    };

    return map
}

Hope that's helpful.

Regards,
Bill

Collapse
 
brittanmcg profile image
Brittan McGinnis

Really great article. Definitely some food for thought. I will keep this in the forefront of my mind the next time I'm in need of a factory.