Creational Design Patterns

Why you shouldn't call an 'ambulance' when a developer starts talking about factories, builders, and singletons.

Time to read: 14 min

Programming is problem-solving. Some problems in everyday work are repeated from project to project. Such problems typically already have solutions — these solutions are called patterns or design templates.

Creational patterns (or templates) help solve problems related to creating entities or groups of similar entities. They eliminate code duplication and make the object creation process shorter and more straightforward.

We will explore the 4 most common patterns:

  • Factory
  • Abstract Factory
  • Builder
  • Singleton

Factory

A factory (Eng. factory) creates an object, freeing us from the need to know the details of creation.

For example, if we need a guitar, we can cut the body, make the strings out of nickel, glue the body, make the neck, place the frets, and tighten the strings... Or we can go to the store and get a guitar made in the factory — in this case, we no longer need to know what exactly needs to be done to create the guitar.

Example

A factory in programming accepts a signal from us that we need to create an object and creates it by encapsulating the creation logic within itself.

        
          
          function createGuitar(stringsCount = 6) {  return {    strings: stringsCount,    frets: 24,    fretBoardMaterial: 'cedar',    boardMaterial: 'maple',  }}
          function createGuitar(stringsCount = 6) {
  return {
    strings: stringsCount,
    frets: 24,
    fretBoardMaterial: 'cedar',
    boardMaterial: 'maple',
  }
}

        
        
          
        
      

In the example, we return a guitar object from the factory function createGuitar(). The function accepts the number of strings as an argument and uses it as the value for the strings field. It fills in all other fields by itself.

In this example, we don't need to know exactly how the field describing the number of strings should be named; we only pass it as an argument. We will get the guitar object like this:

        
          
          const sixStringsGuitar = createGuitar(6)const sevenStringsGuitar = createGuitar(7)
          const sixStringsGuitar = createGuitar(6)
const sevenStringsGuitar = createGuitar(7)

        
        
          
        
      

The advantage of the factory is that knowledge about how to create an object is in one place — inside the factory. If the schema (interface) of the object changes, we only need to change the code in one place — in the factory.

Suppose we now need to:

  • Change the name of the frets field to fretsCount.
  • Change the name of the strings field to stringsCount.
  • Set the default to 7 strings.
  • Change the neck material to fir.
        
          
          function createGuitar(stringsCount = 7) {  return {    stringsCount,    fretsCount: 24,    fretBoardMaterial: 'fir',    boardMaterial: 'maple',  };}
          function createGuitar(stringsCount = 7) {
  return {
    stringsCount,
    fretsCount: 24,
    fretBoardMaterial: 'fir',
    boardMaterial: 'maple',
  };
}

        
        
          
        
      

The places where we actually create objects, that is, call the factory, remain unchanged:

        
          
          const sixStringsGuitar = createGuitar(6)const sevenStringsGuitar = createGuitar(7)
          const sixStringsGuitar = createGuitar(6)
const sevenStringsGuitar = createGuitar(7)

        
        
          
        
      

We are also protected from situations where we need to return instances of a class instead of a simple object:

        
          
          function createGuitar(stringsCount = 6) {  return new Guitar({    strings: stringsCount,    frets: 24,    fretBoardMaterial: 'fir',    boardMaterial: 'maple',  })}
          function createGuitar(stringsCount = 6) {
  return new Guitar({
    strings: stringsCount,
    frets: 24,
    fretBoardMaterial: 'fir',
    boardMaterial: 'maple',
  })
}

        
        
          
        
      

The rest of the code remains the same as it was before.

When to Use

Use a factory if creating an object is more complex than 1-2 lines of code.

This template is especially useful when creating an object requires calculations or obtaining additional data:

        
          
          function createGuitar(strings = 6, maxWeight = 5) {  const fretBoardMaterial = maxWeight <= 5 ? 'fir' : 'cedar'  return {    strings,    frets: 24,    fretBoardMaterial,    boardMaterial: 'maple',  }}
          function createGuitar(strings = 6, maxWeight = 5) {
  const fretBoardMaterial = maxWeight <= 5 ? 'fir' : 'cedar'

  return {
    strings,
    frets: 24,
    fretBoardMaterial,
    boardMaterial: 'maple',
  }
}

        
        
          
        
      

In the example above, we choose the neck material based on the maximum allowed weight. When creating an object requires some logic, it's better to encapsulate it in the factory right away than to repeat the code in different places in the codebase.

Abstract Factory

An abstract factory (Eng. abstract factory) — is a factory of factories 😃

This pattern groups related or similar factories of objects together, allowing you to choose the one needed depending on the situation.

An abstract factory does not return a specific object; instead, it describes the type of object that will be created.

Example

The concept of an abstract factory is easiest to explain using TypeScript and the notion of an interface. Suppose we have an application that manages the musical inventory for a concert orchestra.

The instruments are different, but we can describe them all using the Instrument interface:

        
          
          interface Instrument {  playNote(note: MusicNote): void;}
          interface Instrument {
  playNote(note: MusicNote): void;
}

        
        
          
        
      

We can then describe a violin or a cello like this:

        
          
          class Violin implements Instrument {  playNote(note) {    console.log(`Playing ${note} on the violin!`);  }}class Cello implements Instrument {  playNote(note) {    console.log(`Playing ${note} on the cello!`);  }}
          class Violin implements Instrument {
  playNote(note) {
    console.log(`Playing ${note} on the violin!`);
  }
}

class Cello implements Instrument {
  playNote(note) {
    console.log(`Playing ${note} on the cello!`);
  }
}

        
        
          
        
      

Orchestra musicians strictly play on their instruments, but we can describe all musicians using the Musician interface:

        
          
          interface Musician {  play(piece: MusicPiece): void;}
          interface Musician {
  play(piece: MusicPiece): void;
}

        
        
          
        
      

Then, for example, we can represent violinists and cellists like this:

        
          
          class Violinist implements Musician {  private instrument: Instrument = new Violin()  play = (piece) => piece.forEach((note) => this.instrument.playNote(note))  // Playing A# on the violin!  // Playing C on the violin!  // ...}class Cellist implements Musician {  private instrument: Instrument = new Cello()  play = (piece) => piece.forEach((note) => this.instrument.playNote(note))  // Playing A# on the cello!  // Playing C on the cello!  // ...}
          class Violinist implements Musician {
  private instrument: Instrument = new Violin()

  play = (piece) => piece.forEach((note) => this.instrument.playNote(note))
  // Playing A# on the violin!
  // Playing C on the violin!
  // ...
}

class Cellist implements Musician {
  private instrument: Instrument = new Cello()

  play = (piece) => piece.forEach((note) => this.instrument.playNote(note))
  // Playing A# on the cello!
  // Playing C on the cello!
  // ...
}

        
        
          
        
      

Now let's write part of the application that reserves seats and instruments for each musician in the orchestra.

        
          
          class ViolinReservation {  reserveViolin = () => new Violin()  notifyPlayer = () => new Violinist()}class CelloReservation {  reserveCello = () => new Cello()  notifyPlayer = () => new Cellist()}
          class ViolinReservation {
  reserveViolin = () => new Violin()
  notifyPlayer = () => new Violinist()
}

class CelloReservation {
  reserveCello = () => new Cello()
  notifyPlayer = () => new Cellist()
}

        
        
          
        
      

Let the seats be reserved by a function reserve(). A problem arises when we want to use a common function with different classes to reserve seats. It is unclear what type the argument should be, and it is also unclear which method to call to reserve the instrument:

        
          
          // In the argument, we can use a union of types,// but if another class is added,// we'll have to update this union too :–(function reserve(reservation: ViolinReservation | CelloReservation): void {  // Notify the musician, we can do:  reservation.notifyPlayer()  // But for calling the method to reserve the instrument,  // we need to know which class we have :–(  if (reservation instanceof ViolinReservation) {    reservation.reserveViolin()  } else if (reservation instanceof CelloReservation) {    reservation.reserveCello()  }}
          // In the argument, we can use a union of types,
// but if another class is added,
// we'll have to update this union too :–(
function reserve(reservation: ViolinReservation | CelloReservation): void {
  // Notify the musician, we can do:
  reservation.notifyPlayer()

  // But for calling the method to reserve the instrument,
  // we need to know which class we have :–(
  if (reservation instanceof ViolinReservation) {
    reservation.reserveViolin()
  } else if (reservation instanceof CelloReservation) {
    reservation.reserveCello()
  }
}

        
        
          
        
      

Such a function will be very fragile and will require updating when the composition of the orchestra changes. We want to create one interface for reserving any instruments. This interface will ensure that no matter how many musicians there are or what they are, we can always call one method for reservation.

The abstract factory is just the solution for this problem:

        
          
          // Common interface:interface ReservationFactory {  reserveInstrument(): Instrument;  notifyPlayer(): Musician;}// Implementations for different instruments:class ViolinReservation implements ReservationFactory {  reserveInstrument = () => new Violin()  notifyPlayer = () => new Violinist()}class CelloReservation implements ReservationFactory {  reserveInstrument = () => new Cello()  notifyPlayer = () => new Cellist()}
          // Common interface:
interface ReservationFactory {
  reserveInstrument(): Instrument;
  notifyPlayer(): Musician;
}

// Implementations for different instruments:
class ViolinReservation implements ReservationFactory {
  reserveInstrument = () => new Violin()
  notifyPlayer = () => new Violinist()
}

class CelloReservation implements ReservationFactory {
  reserveInstrument = () => new Cello()
  notifyPlayer = () => new Cellist()
}

        
        
          
        
      

Then the reserve() function becomes more straightforward and less fragile:

        
          
          function reserve(reservation: ReservationFactory): void {  reservation.notifyPlayer()  reservation.reserveInstrument()}
          function reserve(reservation: ReservationFactory): void {
  reservation.notifyPlayer()
  reservation.reserveInstrument()
}

        
        
          
        
      

Since the interface remains the same, we can use it when working with any instruments. Thus, we move away from creating concrete objects, replacing them with an abstraction — their type or interface.

When to Use

If there is a common logic for creating related or similar but not identical objects in the application, the abstract factory helps eliminate duplication and encapsulate creation rules within itself.

Builder

Builder (or constructor) (Eng. builder) allows creating objects by adding properties to them by specified rules. It is useful when creating an object requires many steps, some of which may be optional.

Example

Suppose we are writing a coffee drink constructor. All of them are based on espresso, but there can be many additional ingredients.

        
          
          class Drink {  constructor(settings) {    const { base, milk, sugar, cream } = settings    this.base = base    this.milk = milk    this.sugar = sugar    this.cream = cream  }}
          class Drink {
  constructor(settings) {
    const { base, milk, sugar, cream } = settings

    this.base = base
    this.milk = milk
    this.sugar = sugar
    this.cream = cream
  }
}

        
        
          
        
      

We can add milk, sugar, and cream.

To make it convenient to create drink objects, we will tell the builder step by step what to add to the coffee:

        
          
          class DrinkBuilder {  settings = {    base: 'espresso',  }  addMilk = () => {    this.settings.milk = true    return this  }  addSugar = () => {    this.settings.sugar = true    return this  }  addCream = () => {    this.settings.cream = true    return this  }  addSyrup = () => {    this.settings.syrup = true    return this  }  build = () => new Drink(this.settings)}
          class DrinkBuilder {
  settings = {
    base: 'espresso',
  }

  addMilk = () => {
    this.settings.milk = true
    return this
  }

  addSugar = () => {
    this.settings.sugar = true
    return this
  }

  addCream = () => {
    this.settings.cream = true
    return this
  }

  addSyrup = () => {
    this.settings.syrup = true
    return this
  }

  build = () => new Drink(this.settings)
}

        
        
          
        
      

By default, we add only espresso to the settings, but when calling add...() methods, we add a new ingredient to the settings. On calling build(), we return the assembled drink:

        
          
          const latte = new DrinkBuilder().addMilk().build()const withSugarAndCream = new DrinkBuilder().addSugar().addCream().build()
          const latte = new DrinkBuilder().addMilk().build()
const withSugarAndCream = new DrinkBuilder().addSugar().addCream().build()

        
        
          
        
      

Note that we can chain the add...() methods, ending with the build() call. This is possible because each of the add...() methods returns the current instance of the builder.

        
          
          // ...addMilk = () => {  this.settings.milk = true  // Return the current builder:  return this};
          // ...

addMilk = () => {
  this.settings.milk = true

  // Return the current builder:
  return this
};

        
        
          
        
      

Thus, we use useful functionality — we add an ingredient to the settings and then return this — referring to ourselves to apply a new method of this class.

When to Use

If the application requires creating objects with different features, or the process of creating an object is divided into separate steps, then the builder helps avoid cluttering the code with conditions and checks.

Singleton

A singleton (Eng. singleton) is a pattern that allows creating only one object, and upon attempting to create a new one, it returns the already created one.

Example

Suppose we are writing an application to describe the Solar System. There can be only one Sun, so we can create it only once.

If there is code in the application that tries to create the Sun again for some reason, our class will return the existing object instead of creating another one.

        
          
          class Sun {  // Store a reference to the created object:  static #instance = null  // Make the constructor private:  constructor() {    // If the object was created earlier, return it:    if (Sun.#instance) {      return Sun.#instance    }    // Otherwise, assign the current value of this to the object:    Sun.#instance = this  }}
          class Sun {
  // Store a reference to the created object:
  static #instance = null

  // Make the constructor private:
  constructor() {
    // If the object was created earlier, return it:
    if (Sun.#instance) {
      return Sun.#instance
    }

    // Otherwise, assign the current value of this to the object:
    Sun.#instance = this
  }
}

        
        
          
        
      

We will use such a singleton like this:

        
          
          // On the first call, a new object will be created:const sun = new Sun()// In the future, the instance will return// the previously created object:const sun1 = new Sun()const sun2 = new Sun()console.log(sun === sun1)// trueconsole.log(sun === sun2)// true
          // On the first call, a new object will be created:
const sun = new Sun()

// In the future, the instance will return
// the previously created object:
const sun1 = new Sun()
const sun2 = new Sun()

console.log(sun === sun1)
// true
console.log(sun === sun2)
// true

        
        
          
        
      

When to Use

When you need to ensure strictly one instance of an object for the entire application. This is often unnecessary.

Before using a singleton, consider redesigning the program so that there is no need to use a singleton.

Do Not Confuse With

Singleton, as a pattern, and singleton, as a type of lifecycle for objects in dependency injection — are different things.

The first restricts the number of objects, the second determines which objects and how they will be injected as dependencies into other objects. You can read more about dependency injection in the article “Dependency Injection with TypeScript in Practice”.

Other Patterns

We have explored the most common of the creational design patterns. There are a few more, but the rest are used less frequently.

In addition to creational, there are also other types of design patterns:

  • Structural — help solve problems related to how to combine and blend entities together.
  • Behavioral — distribute responsibilities among modules and determine how communication will take place.