Skip to article
ALGORITHMICSPatterns
Patterns6 min read

Factory Method

Letting a subclass decide what to construct — and why a plain function is usually enough.


An import pipeline reads a file, validates the rows, and writes them to the database. Everything is shared except the reading, which depends on the format.

What people write first

A switch on a format string.

function importFile(kind: string, raw: string) {
let reader: Reader;
if (kind === 'csv') reader = new CsvReader(raw);
else if (kind === 'json') reader = new JsonReader(raw);
else if (kind === 'xml') reader = new XmlReader(raw);
else throw new Error(`unknown format: ${kind}`);
return validate(reader.rows());
}

The problem is not this function. It is that the same switch reappears in preview, in estimateRowCount, in the format-specific error messages — and adding a fourth format means finding every copy. Miss one and it fails at runtime for that format only.

The pattern

Put the shared algorithm in a base class, and leave a hole where construction goes.

function parse(kind: string, raw: string) {
  if (kind === "csv")  return new CsvReader(raw).rows();
  if (kind === "json") return new JsonReader(raw).rows();
  if (kind === "xml")  return new XmlReader(raw).rows();
  throw new Error(`unknown format: ${kind}`);
}

// …and the same three-way switch in validate(), in export(),
// in preview() — each one a place to forget the fourth format.

Every new format edits a function that already works — and the same switch tends to appear in three other files.

abstract class Importer {
/** The hole. Subclasses fill it; nothing else varies. */
protected abstract createReader(raw: string): Reader;
run(raw: string) {
const reader = this.createReader(raw);
return this.validate(reader.rows());
}
}
class CsvImporter extends Importer {
protected createReader(raw: string) { return new CsvReader(raw); }
}

run is written once and works for every format that will ever exist. A new format is a new subclass, and the compiler will not let you write one that forgets createReader.

Where the switch went

It did not disappear. Something still has to pick the subclass:

const IMPORTERS = {
csv: () => new CsvImporter(),
json: () => new JsonImporter(),
xml: () => new XmlImporter(),
};
const importer = IMPORTERS[kind]?.();
if (!importer) throw new Error(`unknown format: ${kind}`);

The lighter versions

If the only thing varying is construction, a function is enough:

type CreateReader = (raw: string) => Reader;
function runImport(createReader: CreateReader, raw: string) {
return validate(createReader(raw).rows());
}
runImport((raw) => new CsvReader(raw), input);

That is Strategy again, and it is usually the better choice — no inheritance, no abstract class, and the dependency is visible in the signature.

And when construction genuinely needs a name rather than a new, a static method does the job with none of the pattern:

class Duration {
private constructor(readonly ms: number) {}
static seconds(n: number) { return new Duration(n * 1000); }
static minutes(n: number) { return new Duration(n * 60_000); }
}

Duration.minutes(5) reads better than a constructor with a unit argument, and the private constructor means there is no second way to build one.

Neighbours

Abstract Factory is this pattern with several related creation methods on one object, so that the products are guaranteed to match each other.

Builder is for a single complex object assembled in steps, not a choice between types.

Prototype sidesteps construction entirely by copying an existing instance.