Skip to main content

Factory Method and Abstract Factory — Moving Creation into Functions

This chapter separates the conditions under which creation can move into a function from the conditions that call for Abstract Factory.

The problem this pattern set out to solve

GoF catalogs these two as separate patterns1.

  • Factory Method — define the procedure for creating an object, and let subclasses decide which class actually gets created.
  • Abstract Factory — create a whole family of related objects together without naming their concrete classes.

The motivation behind both is the same: to peel a concrete creation such as new PngEncoder() away from the code that uses it. When the calling side names concrete classes directly, every added kind means rewriting the calling side.

A premise on the language side is at work here as well. In C++, which GoF used as its main example, you could not carry a procedure that captured state around as a value (there are function pointers, but they cannot hold the surrounding variables). The practical way to swap a creation procedure was class inheritance and method overriding, so it took a heavy tool: a class hierarchy dedicated to creation.

Writing it straightforwardly with classes

The subject here is generating image thumbnails. Written the way Factory Method describes, a hierarchy grows on the creating side too.

type EncodedImage = {mimeType: string; bytes: Uint8Array};

interface ImageEncoder {
encode(pixels: Uint8Array): EncodedImage;
}

class PngEncoder implements ImageEncoder {
encode(pixels: Uint8Array): EncodedImage {
return {mimeType: 'image/png', bytes: pixels}; // Encoding details omitted
}
}

class WebpEncoder implements ImageEncoder {
encode(pixels: Uint8Array): EncodedImage {
return {mimeType: 'image/webp', bytes: pixels}; // Encoding details omitted
}
}

// Creator. Delegates the decision of which class to create to subclasses
abstract class ThumbnailJob {
protected abstract createEncoder(): ImageEncoder;

run(pixels: Uint8Array): EncodedImage {
return this.createEncoder().encode(pixels);
}
}

class PngThumbnailJob extends ThumbnailJob {
protected createEncoder(): ImageEncoder {
return new PngEncoder();
}
}

class WebpThumbnailJob extends ThumbnailJob {
protected createEncoder(): ImageEncoder {
return new WebpEncoder();
}
}

Adding one encoder means adding one encoder class and one job class. The hierarchy grows in two places for every format.

Replacing it with language features

In TypeScript functions are values, so you can pass the creation procedure directly. The two-tier arrangement above folds into one function type and three functions.

type EncodedImage = {mimeType: string; bytes: Uint8Array};
type ImageFormat = 'png' | 'webp' | 'avif';
type Encode = (pixels: Uint8Array) => EncodedImage;

const encodePng: Encode = (pixels) => ({mimeType: 'image/png', bytes: pixels});
const encodeWebp: Encode = (pixels) => ({mimeType: 'image/webp', bytes: pixels});
const encodeAvif: Encode = (pixels) => ({mimeType: 'image/avif', bytes: pixels});

function selectEncoder(format: ImageFormat): Encode {
switch (format) {
case 'png':
return encodePng;
case 'webp':
return encodeWebp;
case 'avif':
return encodeAvif;
default: {
// If you add a format and forget to fix this, this line becomes a type error
const exhaustive: never = format;
return exhaustive;
}
}
}

// The calling side only has to receive how to create it
function makeThumbnail(pixels: Uint8Array, encode: Encode): EncodedImage {
return encode(pixels);
}

The never in the default clause is the form the TypeScript Handbook presents as an exhaustiveness check.

The never type is assignable to every type; however, no type is assignable to never (except never itself). This means you can use narrowing and rely on never turning up to do exhaustive checking in a switch statement.2

If you add a format and forget to fix selectEncoder, a new value remains in format, so the assignment to never becomes a type error. In the class hierarchy version, the compiler says nothing when you forget to write the job class. The job of detecting what you forgot to write has moved from the hierarchy to the type, and that is what this rewrite comes down to.

When you want a family to stay together

What Abstract Factory addresses is having several products whose combination must not go wrong. Things breaking when the encoder and the file extension are mismatched is one such situation. That constraint can be expressed with a function that returns the family as a unit.

type EncodedImage = {mimeType: string; bytes: Uint8Array};
// Narrowed to two formats to keep this short (the previous example had three, including avif)
type ImageFormat = 'png' | 'webp';

type ImageToolkit = {
encode: (pixels: Uint8Array) => EncodedImage;
fileExtension: string;
supportsTransparency: boolean;
};

// Making the Record keys a union of string literal types turns a missing format into a type error right here
const toolkits: Record<ImageFormat, ImageToolkit> = {
png: {
encode: (pixels) => ({mimeType: 'image/png', bytes: pixels}),
fileExtension: '.png',
supportsTransparency: true,
},
webp: {
encode: (pixels) => ({mimeType: 'image/webp', bytes: pixels}),
fileExtension: '.webp',
supportsTransparency: true,
},
};

export function selectToolkit(format: ImageFormat): ImageToolkit {
return toolkits[format];
}

Because the family is closed inside a single object, there is no way to write the png encoder paired with the .webp extension by mistake. The consistency GoF guaranteed with a set of abstract classes is guaranteed here by the shape of the object.

How this series judges it

Verdict on Factory Method: language features can replace it. If all you want is to swap the creation procedure, passing a function or looking a function up by kind will do. In TypeScript there is normally no reason to build a class hierarchy on the creating side.

The exception is when a class hierarchy already exists and each of its subclasses needs to create the part that corresponds to it. There you are not building a new hierarchy, only adding one creation method to an existing one, so the added cost is close to nothing.

Verdict on Abstract Factory: conditional. It only earns its place when both of the following hold.

  • There are several products, and things break unless they all come from the same family
  • The family is decided at runtime (switched by configuration, environment, a user's choice, and so on)

If there is only one product, that is Factory Method's territory, not Abstract Factory's. If the family is fixed, you do not need a switching mechanism in the first place.

What would overturn this judgment

Moving creation into a function gets weaker when every creation needs heavy setup. If reuse of the product is assumed, as with establishing a connection or loading a model, you need something to hold the created result. That turns into a choice between a function that returns an object and a class.

Forcing exhaustiveness with Record also assumes the key set is closed. If formats grow at runtime — registered later as plugins — Record is out, and you end up with a registry combined with a default.

How this relates to existing articles

The word "factory" shows up in other articles on this site as well. They point at different things, so here is the distinction.

  • The "factory method pattern" in Entities refers to making the constructor private and providing named creation methods such as create() and reconstruct(). It is an idiom for giving the intent of creation a name, not an arrangement that delegates the decision to subclasses the way GoF's Factory Method does. This chapter approaches the term from the GoF side, so the same word points at something else.
  • Naming conventions 2: functions and classes lists ~Factory as a class-name suffix that signals object creation. If you follow this chapter's judgment and write a function instead, that function takes a verb name such as create~ or select~.

The basics of discriminated unions and exhaustiveness checks are covered in Conditionals. This chapter is an example of using them in the context of creation.

Footnotes

  1. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). Factory Method and Abstract Factory are both catalogued there as creational patterns. The summary of the intent is this guide's own; we have not checked it against the original text. That the book uses C++ and Smalltalk as its examples was confirmed via a secondary source (source: Design Patterns (Wikipedia)).

  2. Source: Narrowing (TypeScript Handbook), "Exhaustiveness checking".