The Power of Flyweight Design Pattern in JavaScript

Christopher T.

June 14th, 2022

Share This Post

In JavaScript we are fortunate to have an automatic garbage collection mechanism built into the language. There are some cases where it is essential to manage the memory ourselves. This is where the Flyweight Design Pattern can come in handy as it intends to share commonalities into objects that clients can benefit from. This is an efficient way to write scalable applications as it benefits us to allow users to consume the least amount of memory usage as possible.

In this post we will be going over the power of the the Flyweight Design Pattern in JavaScript and leverage it to create more memory efficient applications. We will go over the problems that arise and showcase how the flyweight pattern knocks them all away.

If you've used a JavaScript library before, there's a good chance you've worked directly on some variation of a flyweight pattern given to you whether it was through a JavaScript library, framework, or even the DOM.

Lets take a look at this array of objects that represent objects as DOM elements:

const elems = [
  {
    tagName: 'div',
    style: { width: '28.5px', height: '20px' },
    children: [
      { tagName: 'input', style: { width: '28.5px', height: '20px' } },
      { tagName: 'input', style: { width: '28.5px', height: '20px' } },
      { tagName: 'select', style: { width: '28.5px', height: '20px' } },
      { tagName: 'input', style: { width: '28.5px', height: '20px' } },
    ],
  },
]

If you look at the children array notice that there are three objects that are structurally identical:

flyweight-elems-three-identical-structurally-equivalent-objects.png

This is already an issue because if we were to continue this practice and our project grows larger our program will take a big hit in performance because it will create three separate objects in memory although they are all structurally equivalent. Imagine if there were 1000?

When we go over real examples of the flyweight design pattern this is basically what the flyweight intends to do behind the scenes:

const inputElement = {
  tagName: 'input',
  style: { width: '28.5px', height: '20px' },
}

const elems = [
  {
    tagName: 'div',
    style: { width: '28.5px', height: '20px' },
    children: [
      inputElement,
      inputElement,
      { tagName: 'select', style: { width: '28.5px', height: '20px' } },
      inputElement,
    ],
  },
]

Notice how inputElement is mentioned multiple times.

We will go over examples in different object structures (like classes for example) but ultimately this concept is always applied.


A good way to think of the flyweight pattern is "things being shared". In our previous example we shared the inputElement object three times. We minimized the use of memory by at least three occasions.

A common implementation you might encounter frequently are those that implement some sort of get method to retrieve objects in some cache in memory:

class Coin {
  constructor(value) {
    this.value = value
  }
}

class CoinCollage {
  coins = new Map()

  create(value) {
    let coin = this.coins.get(value)
    if (!coin) {
      coin = new Coin(value)
      this.coins.set(value, coin)
    }
    return coin
  }
}

const coins = new CoinCollage()

const dime = coins.create(0.1)
const quarter = coins.create(0.25)
const dollar = coins.create(1.0)

console.log(coins)

This is a great technique to reuse and share previously created objects that don't need to be recreated since it preserves the user's memory.

It's used in many libraries like ts-morph which usually prefix those methods with something like "getOrCreate<the rest of the variable's name>"

Intrinsic State

Flyweight implementations usually become useful when we add some intrinsic state to it where we can leverage it to make memory efficient decisions.

In our previous examples if we look at our CoinCollage class we can spot our intrinsic state here:

flyweight-design-pattern-intrinsic-state

We first attempted to grab a previously created Coin instance with the value asked for. If our application created it previously we can avoid the unnecessary re-creation and just return the previous Coin we had stored.

This is a benefit that library authors most often seek.

Extrinsic State

Another important role to the flyweight pattern is extrinsic state. These are states that exist outside of the flyweight implementation but would like to work with the flyweight. A common use case are states that are observed by callback functions:

class CoinCollage {
  #onCreate = undefined
  coins = new Map()

  create(value) {
    let coin = this.coins.get(value)
    if (!coin) {
      coin = new Coin(value)
      this.#onCreate?.(coin)
      this.coins.set(value, coin)
    }
    return coin
  }

  set onCreate(fn) {
    this.#onCreate = fn
  }
}

const coins = new CoinCollage()

const createdCoinValues = []

coins.onCreate = function onCreate(coin) {
  createdCoinValues.push(coin)

  console.log(
    `Created new coin of value ${coin.value}. The total number of coins is now ${createdCoinValues.length}`,
  )

  if (createdCoinValues.length >= 3) {
    console.warn(`You are 2 coins away from the maximum coins allowed`)
  }
}

const dime = coins.create(0.1)
const quarter = coins.create(0.25)
const dollar = coins.create(1.0)

flyweight-design-pattern-extrinsic-state.png

With this capability in place we can halt further creations of Coin if our business logic only applies to the first 5 coins. This is a great companion to our instrinsic state!

Prototypal Inheritance

Something that was hard for me to grasp in the earlier stages of my JavaScript development career was the decision making between prototypal inheritance and factory functions. A mystery that dwelled onto me for way too long was figuring out why I often saw functions being created this way:

function Calculator() {
  this.value = 0
}

Calculator.prototype.add = function add(num1, num2) {
  return num1 + num2
}

Calculator.prototype.subtract = function subtract(num1, num2) {
  return num2 - num1
}

As opposed to having objects created this way:

function makeCalculator() {
  let value = 0

  return {
    add(num1, num2) {
      return num1 + num2
    },
    subtract(num1, num2) {
      return num2 - num1
    },
  }
}

In the first example, new instantiations of our Calculator class will inherit and re-use the same properties/methods defined on its prototype. That means these:

flyweight-design-pattern-diagram-javascript.png

In the second example, new instantiations of our makeCalculator factory will not inherit and re-use add and subtract but will instead receive an entirely new add and subtract functions even though they are identical in shape and code size.

flyweight-pattern-factory-function-version-in-javascript.png

They both have their pros and cons in general. But in the context of flyweight, prototypal inheritance is recommended. For all else however I always go with the factory function but that's beyond the scope of this post.

Real World Code Example

The superagent library showcases the flyweight design pattern in practice using prototypal inheritance in the Request class. Modern programs make frequent requests to do tasks like data fetching. The thing is, these requests cannot be re-used for subsequent requests. They need to instantiate new instances of some Request object. When programs work with Request objects it's really unnecessary to make copies of its methods and properties that don't rely on the current state.

One example of this is the query method on the Request object. The implementation details hardly ever change so there's no point to create and carry copies to new objects.

Conclusion

And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!


Top online courses in Web Development

Tags


Read every story from jsmanifest (and thousands of other writers on medium)

Your membership fee directly supports jsmanifest and other writers you read. You'll also get full access to every story on Medium.

Subscribe to the Newsletter

Get continuous updates

Mediumdev.toTwitterGitHubrss

© jsmanifest 2023