Emmanuel Akinfulubi

Lesson 04 of 1510 min read

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.

What repetition looks like, and why it is a bug waiting to happen

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.

One function, as many objects as you want

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.

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.

A factory that refuses to build something broken

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.

seatsLeft is unreachable from outside

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.

Each object carries its own methods

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.

The factory picks the implementation

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.

Editable. Fill it in, then run it.

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.

Saved in this browser only.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.