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!
Frequently Asked Questions (FAQs) about Factory Functions in JavaScript
What is the main difference between factory functions and constructor functions in JavaScript?
Factory functions and constructor functions in JavaScript both serve the purpose of creating new objects. However, they differ in their implementation and usage. A factory function is a function that returns an object when it is called, and it’s not new or this. It provides a simple way to create similar objects. On the other hand, a constructor function is used with the new keyword and it uses this to refer to the new object it creates.
Can you provide an example of a factory function in JavaScript?
Sure, here’s a simple example of a factory function in JavaScript:function createPerson(name, age) {
return {
name: name,
age: age,
sayHello: function() {
console.log('Hello, my name is ' + this.name);
}
};
}
var person1 = createPerson('John', 30);
person1.sayHello(); // Outputs: Hello, my name is John
In this example, createPerson is a factory function that creates and returns a new object each time it’s called.
Why should I use factory functions instead of classes in JavaScript?
Factory functions provide several advantages over classes in JavaScript. They promote functional programming style, and they avoid the confusion caused by JavaScript’s ‘new’ and ‘this’ keywords. Factory functions also offer more flexibility and can utilize closures to encapsulate private data. However, whether you should use factory functions or classes depends on your specific use case and personal preference.
How can I create private variables in a factory function?
In JavaScript, you can create private variables in a factory function by utilizing closures. Here’s an example:function createPerson(name, age) {
var secret = 'I am a secret message';
return {
name: name,
age: age,
getSecret: function() {
return secret;
}
};
}
var person1 = createPerson('John', 30);
console.log(person1.getSecret()); // Outputs: I am a secret message
In this example, ‘secret’ is a private variable that can only be accessed through the ‘getSecret’ method.
Can factory functions be used with ES6 arrow functions?
Yes, factory functions can be used with ES6 arrow functions. Here’s an example:const createPerson = (name, age) => ({
name,
age,
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
});
const person1 = createPerson('John', 30);
person1.sayHello(); // Outputs: Hello, my name is John
In this example, createPerson is a factory function defined using an arrow function.
How can I add methods to a factory function?
You can add methods to a factory function by including them in the object that the function returns. Here’s an example:function createPerson(name, age) {
return {
name: name,
age: age,
sayHello: function() {
console.log('Hello, my name is ' + this.name);
}
};
}
var person1 = createPerson('John', 30);
person1.sayHello(); // Outputs: Hello, my name is John
In this example, ‘sayHello’ is a method added to the object returned by the createPerson factory function.
Can factory functions inherit from other factory functions?
Yes, factory functions can inherit from other factory functions. This can be achieved by using Object.assign or the spread operator to combine the properties of multiple objects. Here’s an example:function createLivingBeing(name) {
return {
name,
isAlive: true
};
}
function createPerson(name, age) {
return {
...createLivingBeing(name),
age,
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
};
}
const person1 = createPerson('John', 30);
console.log(person1.isAlive); // Outputs: true
In this example, the createPerson factory function inherits from the createLivingBeing factory function.
What is the role of closures in factory functions?
Closures play a crucial role in factory functions as they allow the function to have private variables and methods. A closure is a function that has access to its own scope, the scope of the outer function, and the global scope. This means that a factory function can use closures to encapsulate data that can’t be accessed directly from the outside, providing a way to implement data privacy.
Can factory functions return other functions?
Yes, factory functions can return other functions. This is often used in functional programming to create higher-order functions. Here’s an example:function createGreeter(greeting) {
return function(name) {
console.log(greeting + ', ' + name);
};
}
var sayHello = createGreeter('Hello');
sayHello('John'); // Outputs: Hello, John
In this example, createGreeter is a factory function that returns a function.
How can I use factory functions with JavaScript modules?
Factory functions can be used with JavaScript modules by exporting the factory function and then importing it in another module. Here’s an example:// person.js
export function createPerson(name, age) {
return {
name,
age,
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
};
}
// main.js
import { createPerson } from './person.js';
const person1 = createPerson('John', 30);
person1.sayHello(); // Outputs: Hello, my name is John
In this example, the createPerson factory function is exported from the person.js module and then imported and used in the main.js module.
Digital Nomad and co-founder of UK based startup Astral Dynamics.