JavaScript - - By Dan Prince

Quick Tip: What Are Factory Functions in JavaScript

This article was peer reviewed by Jeff Mott. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

You can’t get far as a JavaScript programmer without learning about functions and objects, and when used together, they are the building blocks we need to get started with a powerful object paradigm called composition. Today we’ll look at some idiomatic patterns for using factory functions to compose functions, objects and promises.

When a function returns an object, we call it a factory function.

Let’s take a look at a simple example.

function createJelly() {
  return {
    type: 'jelly',
    colour: 'red'
    scoops: 3
  };
}

Each time we call this factory, it will return a new instance of the jelly object.

It’s important to note that we don’t have to prefix our factory names with create but it can make the intent of the function clearer to others. The same is true with the type property but often it can help us differentiate between the objects flowing through our programs.

Parameterized Factory Functions

Like all functions, we can define our factory with parameters which change the shape of the returned object.

function createIceCream(flavour='Vanilla') {
  return {
    type: 'icecream',
    scoops: 3,
    flavour
  }
}

In theory, you could use parameterized factories with hundreds of arguments to return very specific and deeply nested objects, but as we’ll see, that’s not at all in the spirit of composition.

Composable Factory Functions

Defining one factory in terms of another helps us break complex factories into smaller, reusable fragments.

For example, we can create a dessert factory which is defined in terms of the jelly and ice cream factories from before.

function createDessert() {
  return {
    type: 'dessert',
    bowl: [
      createJelly(),
      createIceCream()
    ]
  };
}

We can compose factories to build arbitrarily complex objects that don’t require us to mess around with new or this.

Objects that can be expressed in terms of has-a relationships, rather than is-a can be implemented with composition, instead of inheritance.

For example, with inheritance.

// A trifle *is a* dessert

function Trifle() {
  Dessert.apply(this, arguments);
}

Trifle.prototype = Dessert.prototype;

// or

class Trifle extends Dessert {
  constructor() {
    super();
  }
}

We can express the same idea with composition.

// A trifle *has* layers of jelly, custard and cream. It also *has a* topping.

function createTrifle() {
  return {
    type: 'trifle',
    layers: [
      createJelly(),
      createCustard(),
      createCream()
    ],
    topping: createAlmonds()
  };
}

Async Factory Functions

Not all factories will be ready to return data immediately. For instance, some will have to fetch data first.

In these cases, we can define factories that return promises instead.

function getMeal(menuUrl) {
  return new Promise((resolve, reject) => {
    fetch(menuUrl)
      .then(result => {
        resolve({
          type: 'meal',
          courses: result.json()
        });
      })
      .catch(reject);
  });
}

This kind of deeply nested indentation can make asynchronous factories difficult to read and test. It can often be helpful to break them down into multiple distinct factories, then compose them.

function getMeal(menuUrl) {
  return fetch(menuUrl)
    .then(result => result.json())
    .then(json => createMeal(json));
}

function createMeal(courses=[]) {
  return {
    type: 'meal',
    courses
  };
}

Of course we could have used callbacks instead, but we already have tools like Promise.all for composing factories that return promises.

function getWeeksMeals() {
  const menuUrl = 'jsfood.com/';

  return Promise.all([
    getMeal(`${menuUrl}/monday`),
    getMeal(`${menuUrl}/tuesday`),
    getMeal(`${menuUrl}/wednesday`),
    getMeal(`${menuUrl}/thursday`),
    getMeal(`${menuUrl}/friday`)
  ]);
}

We’re using get rather than create as a naming convention to show that these factories do some asynchronous work and return promises.

Functions & Methods

So far, we haven’t seen any factories that return objects with methods and this is deliberate. This is because generally, we don’t need to.

Factories allow us to separate our data from our computations.

This means we’ll always be able to serialize our objects as JSON, which is important for persisting them between sessions, sending them over HTTP or WebSockets, and putting them into data stores.

For example, rather than defining an eat method on the jelly objects, we can just define a new function which takes an object as a parameter and returns a modified version.

function eatJelly(jelly) {
  if(jelly.scoops > 0) {
    jelly.scoops -= 1;
  }
  return jelly;
}

A little bit of syntactic help makes this a viable pattern for those who prefer to program without mutating data structures.

function eat(jelly) {
  if(jelly.scoops > 0) {
    return { ...jelly, scoops: jelly.scoops - 1 };
  } else {
    return jelly;
  }
}

Now, rather than writing:

import { createJelly } from './jelly';

createJelly().eat();

We’ll write:

import { createJelly, eatJelly } from './jelly';

eatJelly(createJelly());

The end result is a function which takes an object and returns an object.

And what do we call a function that return an object? A factory!

Higher Order Factories

Passing factories around as higher order functions gives us a huge amount of control. For example, we can use this concept to create enhancers.

function giveTimestamp(factory) {
  return (...args) => {
    const instance = factory(...args);
    const time = Date.now();
    return { time, instance };
  };
}

const createOrder = giveTimestamp(function(ingredients) {
  return {
    type: 'order',
    ingredients
  };
});

This enhancer takes an existing factory and wraps it to create a factory which returns instances with timestamps.

Alternatively, if we want to ensure that a factory returns immutable objects, we could enhance it with a freezer.

function freezer(factory) {
  return (...args) => Object.freeze(factory(...args)));
}

const createImmutableIceCream = freezer(createIceCream);

createImmutableIceCream('strawberry').flavour = 'mint'; // Error!

Conclusion

As a wise programmer once said:

It’s much easier to recover from no abstraction than the wrong abstraction.

JavaScript projects have a tendency to become hard to test and refactor because of the intricate layers of abstraction that we are often encouraged to build with.

Prototypes and classes implement a simple idea with complex and unnatural tools like new and this which still cause all kinds of confusion even now—years after they were added to the language.

Objects and functions make sense to programmers from most backgrounds and both are primitive types in JavaScript, so it could be argued that factories aren’t an abstraction at all!

Using these simple building blocks makes our code much friendlier for inexperienced programmers and that is definitely something we should all care about. Factories encourage us to model complex and asynchronous data with primitives that have a natural capacity for composition, without forcing us to reach for high level abstractions either. JavaScript is sweeter when we stick with simplicity!

Sponsors