{"data":{"site":{"siteMetadata":{"title":"Dev Mastery","author":"Bill Sourour"}},"markdownRemark":{"id":"e75f1595-2f9a-5c06-800c-7148bb83f2cb","excerpt":"I’ve been working with JavaScript on and off since the late nineties. I didn’t…","html":"
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.
\nOver 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.
\nI wrote about the first pattern — “RORO” — in the article below. Don’t worry if you haven’t read it, you can read these in any order.
\nElegant patterns in modern JavaScript: RORO
\nToday, I’d like to introduce you to the “Ice Factory” pattern.
\nAn 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.
\nIt 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.
\nIf 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\n\nexport default class ShoppingCart {\n constructor({db}) {\n this.db = db\n }\n\n addProduct (product) {\n this.db.push(product)\n }\n\n empty () {\n this.db = []\n }\n\n get products () {\n return Object\n .freeze(this.db)\n }\n\n removeProduct (id) {\n // remove a product\n }\n\n// other methods\n\n}\n\n// someOtherModule.js\n\nconst db = []\nconst cart = new ShoppingCart({db})\ncart.addProduct({\n name: 'foo',\n price: 9.99\n})
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.
\nJavaScript Classes will bite you if you’re not careful.
\nFor example, objects created using the new
keyword are mutable. So, you can actually re-assign a method:
const db = []\nconst cart = new ShoppingCart({db})\n\ncart.addProduct = () => 'nope!'\n// No Error on the line above!\n\ncart.addProduct({\n name: 'foo',\n price: 9.99\n}) // output: \"nope!\" FTW?
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:
\nconst cart = new ShoppingCart({db: []})\nconst other = new ShoppingCart({db: []})\n\nShoppingCart.prototype .addProduct = () => ‘nope!’\n// No Error on the line above!\n\ncart.addProduct({\n name: 'foo',\n price: 9.99\n}) // output: \"nope!\"\n\nother.addProduct({\n name: 'bar',\n price: 8.88\n}) // output: \"nope!\"
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.
\nConsider our cart.empty
method.
empty () {\n this.db = []\n }
If we assign this method directly to the click
event of a button on our web page…
<button id=\"empty\">\n Empty cart\n</button>\n\n---\n\ndocument\n .querySelector('#empty')\n .addEventListener(\n 'click',\n cart.empty\n )
… 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.
\nTo make it work we have to do:
\ndocument\n .querySelector(\"#empty\")\n .addEventListener(\n \"click\",\n () => cart.empty()\n )
Or:
\ndocument\n .querySelector(\"#empty\")\n .addEventListener(\n \"click\",\n cart.empty.bind(cart)\n )
I think Mattias Petter Johansson said it best:
\n\n\n“
\nnew
andthis
[in JavaScript] are some kind of unintuitive, weird, cloud rainbow trap.”
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:
\n// makeShoppingCart.js\n\nexport default function makeShoppingCart({\n db\n}) {\n return Object.freeze({\n addProduct,\n empty,\n getProducts,\n removeProduct,\n // others\n })\n\nfunction addProduct (product) {\n db.push(product)\n }\n\n function empty () {\n db = []\n }\n\nfunction getProducts () {\n return Object\n .freeze(db)\n }\n\nfunction removeProduct (id) {\n // remove a product\n }\n\n// other functions\n}\n\n// someOtherModule.js\n\nconst db = []\nconst cart = makeShoppingCart({ db })\ncart.addProduct({\n name: 'foo',\n price: 9.99\n})
Notice our “weird, cloud rainbow traps” are gone:
\nWe no longer need new
.\nWe just invoke a plain old JavaScript function to create our cart
object.
We no longer need this
.\nWe can access the db
object directly from our member functions.
Our cart
object is completely immutable.\nObject.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.
Another advantage of Ice Factories is that they can have private members. For example:
\nfunction makeThing(spec) {\n const secret = 'shhh!'\n\nreturn Object.freeze({\n doStuff\n })\n\nfunction doStuff () {\n // We can use both spec\n // and secret in here\n }\n}\n\n// secret is not accessible out here\n\nconst thing = makeThing()\nthing.secret // undefined
This is made possible because of Closures in JavaScript, which you can read more about on MDN.
\nAlthough Factory Functions have been around JavaScript forever, the Ice Factory pattern was heavily inspired by some code that Douglas Crockford showed in this video.\nHere’s Crockford demonstrating object creation with a function he calls “constructor”:
\n \n \n \n \n \n \n
My Ice Factory version of the Crockford example above would look like this:
\nfunction makeSomething({ member }) {\n const { other } = makeSomethingElse()\n\n return Object.freeze({\n other,\n method\n })\n\nfunction method () {\n // code that uses \"member\"\n }\n}
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.
\nI 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.
\nNote: 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.
\nIf 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.
\nAlong 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.
\nBut 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:
\n\n\n“Favor object composition over class inheritance.” \n– Design Patterns: Elements of Reusable Object-Oriented Software.
\n
In fact, the authors of that book — colloquially known as “The Gang of Four” — go on to say:
\n\n\n“…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.”
\n
So, here’s our product list:
\nfunction makeProductList({ productDb }) {\n return Object.freeze({\n addProduct,\n empty,\n getProducts,\n removeProduct,\n // others\n )}\n // definitions for\n // addProduct, etc...\n}
And here’s our shopping cart:
\nfunction makeShoppingCart(productList) {\nreturn Object.freeze({\n items: productList,\n someCartSpecificMethod,\n // ...\n )}\n\nfunction someCartSpecificMethod () {\n // code\n }\n}
And now we can just inject our Product List into our Shopping Cart, like this:
\nconst productDb = []\nconst productList = makeProductList({ productDb })\n\nconst cart = makeShoppingCart(productList)
And use the Product List via the items
property. Like:
cart.items.addProduct()
It may be tempting to subsume the entire Product List by incorporating its methods directly into the shopping cart object, like so:
\nfunction makeShoppingCart({\n addProduct,\n empty,\n getProducts,\n removeProduct,\n ...others\n }) {\nreturn Object.freeze({\n addProduct,\n empty,\n getProducts,\n removeProduct,\n someOtherMethod,\n ...others\n )}\n\nfunction someOtherMethod () {\n // code\n }\n}
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.
\n \n \n \n \n \n \n
Careful
\nWhenever 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.”
\nThe 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.
\nMaking objects with an Ice Factory is slower and takes up more memory than using a class.
\nIn 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.
\nIf 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.
\nJust remember, profile your app first and don’t prematurely optimize. Most of the time, object creation is not going to be the bottleneck.
\nDespite 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.
\nFinally, I need to acknowledge that I’ve made a bunch of opinionated style choices in the code samples I’ve presented to you:
\nmakeX
instead of createX
or buildX
or something else.You may make different style choices, and that’s okay! The style is not the pattern.
\nThe 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.
","wordCount":{"words":1518},"frontmatter":{"title":"Elegant patterns in modern JavaScript: Ice Factory","date":"March 13, 2018","author":"Bill Sourour","spoiler":"Inspired by Douglas Crockford, the \"Ice Factory\" pattern in JavaScript is a modern take on Factory Functions that will make your code clean and resilient.","topic":"JavaScript","category":"Article","imageCaption":"\"Cold latte in a glass cup\" by Demi DeHerrera on Unsplash.com","imageDescription":"Cold latte with ice in a glass cup","image":{"childImageSharp":{"fluid":{"aspectRatio":1.6470588235294117,"src":"/static/e7e77ea2b45b4c2735891f7c561d6046/9aef4/ice-factory.jpg","srcSet":"/static/e7e77ea2b45b4c2735891f7c561d6046/3683b/ice-factory.jpg 140w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/7601b/ice-factory.jpg 280w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/9aef4/ice-factory.jpg 560w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/b1ee7/ice-factory.jpg 840w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/ae9ac/ice-factory.jpg 1120w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/116a6/ice-factory.jpg 1680w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/ad48b/ice-factory.jpg 2000w","srcWebp":"/static/e7e77ea2b45b4c2735891f7c561d6046/e3646/ice-factory.webp","srcSetWebp":"/static/e7e77ea2b45b4c2735891f7c561d6046/1d88e/ice-factory.webp 140w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/c8d9f/ice-factory.webp 280w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/e3646/ice-factory.webp 560w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/1fea3/ice-factory.webp 840w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/0c650/ice-factory.webp 1120w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/fab0b/ice-factory.webp 1680w,\n/static/e7e77ea2b45b4c2735891f7c561d6046/b9307/ice-factory.webp 2000w","sizes":"(max-width: 560px) 100vw, 560px"}}}}}},"pageContext":{"dmPostId":"bc38fb281731b8ed840afa4140e69a21","slug":"/blog/elegant-patterns-in-modern-javascript-ice-factory/","previous":{"fields":{"slug":"/blog/elegant-patterns-in-modern-javascript-roro/","dmPostId":"d59e7189b58cea520f4b7049f4318ab1"},"frontmatter":{"title":"Elegant patterns in modern JavaScript: RORO","image":{"childImageSharp":{"fluid":{"base64":"data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAMABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAMEAgX/xAAVAQEBAAAAAAAAAAAAAAAAAAACAf/aAAwDAQACEAMQAAABgq5rAlGBz//EABkQAQADAQEAAAAAAAAAAAAAAAIBAxIAEP/aAAgBAQABBQKkbVtWInipMuxPz//EABURAQEAAAAAAAAAAAAAAAAAABAh/9oACAEDAQE/AYf/xAAWEQADAAAAAAAAAAAAAAAAAAAQESH/2gAIAQIBAT8BrH//xAAZEAACAwEAAAAAAAAAAAAAAAAAARARIjH/2gAIAQEABj8Co7Fo04//xAAYEAADAQEAAAAAAAAAAAAAAAAAAREhMf/aAAgBAQABPyFS7hs8EUVGxo16QbP/2gAMAwEAAgADAAAAEEvP/8QAFhEBAQEAAAAAAAAAAAAAAAAAARAR/9oACAEDAQE/EBxP/8QAFxEAAwEAAAAAAAAAAAAAAAAAARAhEf/aAAgBAgEBPxAimxf/xAAZEAEAAwEBAAAAAAAAAAAAAAABABEhMeH/2gAIAQEAAT8Q7GC1hGdbxQfYrexQu4SV6+QVhEuf/9k=","tracedSVG":"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='400' height='230' viewBox='0 0 400 230' version='1'%3e%3cpath d='M0 115v115h401V0h-78a3141 3141 0 0 0-84 2h-2l2-1-118-1H0v115m173-64l-4 1-3 1-13 6a71 71 0 0 0-37 73c7 42 47 70 89 60 26-5 47-26 53-51 2-12 0-10 21-12h11v-17l-3-1h-24c-4 1-4 1-4-3l-11-26c-9-13-23-23-39-29-8-2-26-4-36-2m0 6a232 232 0 0 1-13 3l-4 2c-4 1-10 6-12 9l-3 1v1l-2 4c-3 2-3 3-2 3h2c0-2 7-8 10-9l2-1 2-1 5-2 6-3 3-1 16-2 4-1h8c8 0 24 4 32 10l4 2-1-2-4-3c-4-3-19-9-26-11-7-1-20-1-27 1m-19 121c0 1 9 6 15 8 13 4 38 4 42-1l2-1 5-3 2-3-3 1h-3c-2 0-2 0-1 2l-6 1c-5-1-6-1-7 1h-1l-1-1v1l-2 1-5 2c-5 2-7 1-7-1s0-2-3-2h-3l-5-1c-4 1-5 1-4-1h-2c-2 1-8 0-7-2 1-1 0-1-2-1h-4' fill='lightgray' fill-rule='evenodd'/%3e%3c/svg%3e","aspectRatio":1.6666666666666667,"src":"/static/e742adb7699341f57bfa21601e7099dc/81b2d/roro.jpg","srcSet":"/static/e742adb7699341f57bfa21601e7099dc/de3a0/roro.jpg 200w,\n/static/e742adb7699341f57bfa21601e7099dc/fcaec/roro.jpg 400w,\n/static/e742adb7699341f57bfa21601e7099dc/81b2d/roro.jpg 800w,\n/static/e742adb7699341f57bfa21601e7099dc/d6940/roro.jpg 1000w","srcWebp":"/static/e742adb7699341f57bfa21601e7099dc/9fbef/roro.webp","srcSetWebp":"/static/e742adb7699341f57bfa21601e7099dc/f9bb4/roro.webp 200w,\n/static/e742adb7699341f57bfa21601e7099dc/28d7d/roro.webp 400w,\n/static/e742adb7699341f57bfa21601e7099dc/9fbef/roro.webp 800w,\n/static/e742adb7699341f57bfa21601e7099dc/9e4d6/roro.webp 1000w","sizes":"(max-width: 800px) 100vw, 800px","originalImg":"/static/e742adb7699341f57bfa21601e7099dc/d6940/roro.jpg","originalName":"roro.jpg","presentationWidth":800,"presentationHeight":460}}}}},"next":null}}