Factory Functions
You need forty buses, not one. A factory is just a function that builds and hands back a finished object, and it is the simplest way to stop repeating yourself.
By the end: Build many similar objects from one function.
One object at a time is fine when you have one object. The park has forty.
const lagos = {
plate: 'LAG-441-XA',
seatsLeft: 9,
describe() { return this.plate + ' / ' + this.seatsLeft; },
};
const abuja = {
plate: 'ABJ-772-KM',
seatsLeft: 6,
describe() { return this.plate + ' / ' + this.seatsLeft; },
};
// Thirty-eight more, and when `describe` needs changing you will
// change thirty-nine copies and miss two.
A factory function is the plainest fix in the language. It is an ordinary function that builds an object and returns it. No new keywords, nothing to learn.
function makeBus(plate, seatsLeft) {
return {
plate,
seatsLeft,
describe() {
return this.plate + ' has ' + this.seatsLeft + ' seats left';
},
};
}
const lagos = makeBus('LAG-441-XA', 9);
const abuja = makeBus('ABJ-772-KM', 6);
console.log(lagos.describe());
console.log(abuja.describe());
console.log('Separate objects?', lagos !== abuja);
Output
Three things to notice.
- It is called like any other function,
makeBus(...), with nonewin front of it. plateandseatsLeftuse property shorthand, since the parameter names already match.- Every call returns a genuinely separate object.
Defaults, validation, derived values
Because a factory is just a function, everything you already know about functions applies. This is its real advantage over hand-written literals: there is one place to put the rules.
function makeBus({ plate, seats = 14, kind = 'coaster' }) {
if (!plate) {
throw new Error('A bus needs a plate number.');
}
if (seats < 1) {
throw new Error(`${plate}: a bus with ${seats} seats cannot carry anybody.`);
}
return {
plate,
seats,
kind,
// Derived at build time, from the arguments.
size: seats > 20 ? 'full coach' : 'minibus',
describe() {
return `${this.plate}, a ${this.size} ${this.kind}`;
},
};
}
console.log(makeBus({ plate: 'LAG-441-XA', seats: 49 }).describe());
console.log(makeBus({ plate: 'ABJ-772-KM' }).describe());
try {
makeBus({ plate: 'BAD-000-XX', seats: 0 });
} catch (err) {
console.log('Refused:', err.message);
}
Output
Genuinely private data, for free
Here is the trick that makes factories more than a convenience. A variable declared inside the factory is not a property of the returned object. Only the methods you return can see it. That is a closure, and it gives you privacy that nothing outside can reach.
function makeBus(plate, initialSeats) {
// A local variable rather than a property, so only the methods
// returned below can ever see it.
let seatsLeft = initialSeats;
let hasBookings = false;
return {
plate,
seatsAvailable() {
return seatsLeft;
},
book(n) {
if (n > seatsLeft) {
throw new Error(`Only ${seatsLeft} seats left; cannot book ${n}.`);
}
seatsLeft -= n;
hasBookings = true;
return this;
},
anyBookings() {
return hasBookings;
},
};
}
const b = makeBus('LAG-441-XA', 9);
console.log(b.seatsAvailable());
b.book(3);
console.log('after booking 3:', b.seatsAvailable(), '| any bookings:', b.anyBookings());
// There is no property to reach for, so nothing here relies on convention.
console.log('b.seatsLeft is', b.seatsLeft);
console.log('own keys:', Object.keys(b));
try {
b.book(99);
} catch (err) {
console.log('Refused:', err.message);
}
Output
The cost, stated honestly
Factories have one real downside: every object gets its own fresh copy of every method. Ten thousand buses means ten thousand separate describe functions doing identical work.
function makeBus(plate) {
return {
plate,
describe() { return this.plate; },
};
}
const a = makeBus('A');
const b = makeBus('B');
console.log('same function object?', a.describe === b.describe);
console.log('methods are own properties:', Object.keys(a));
Output
For most code this genuinely does not matter, and the clarity is worth more than the memory. But it is the exact problem prototypes solve, and prototypes are why class exists. That is the next two lessons.
A factory that returns different shapes
One thing a factory can do that a constructor cannot: decide what kind of object to hand back.
function makeVehicle(kind) {
const base = {
kind,
describe() { return `${this.kind}: ${this.seats()} seats`; },
};
if (kind === 'luxury') {
return { ...base, seats: () => 49, comfort() { return 'air conditioned'; } };
}
if (kind === 'coaster') {
return { ...base, seats: () => 30, comfort() { return 'fans only'; } };
}
return { ...base, seats: () => 7 };
}
for (const kind of ['luxury', 'coaster', 'sienna']) {
const v = makeVehicle(kind);
console.log(v.describe());
}
Output
Your turn: build the factory
Write makeAccount(owner, startingBalance). The balance must be a closure variable, not a property. Give it deposit(n), withdraw(n) which refuses to overdraw, and balance(). Then prove from outside that the balance cannot be reached or set directly.
Output
Recap
- A factory is any ordinary function that builds an object and returns it, called without
new. - Because it is a function, you get defaults, validation and derived values in one place.
- Variables declared inside the factory are truly private; only the returned methods can see them.
- The trade-off is that every object carries its own copy of every method.
- Factories can decide what shape of object to return, which a constructor cannot.