Home Programming JavaScript Design Patterns

JavaScript Design Patterns

By Hugo Di Francesco
books-svg-icon Book
eBook $31.99 $21.99
Print $39.99
Subscription $15.99 $10 p/m for three months
$10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime!
What do you get with a Packt Subscription?
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
BUY NOW $10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime!
eBook $31.99 $21.99
Print $39.99
Subscription $15.99 $10 p/m for three months
What do you get with a Packt Subscription?
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
  1. Free Chapter
    Chapter 1: Working with Creational Design Patterns
About this book
Unlock the potential of JavaScript design patterns, the foundation for development teams seeking structured and reusable solutions to common software development challenges in this guide to improving code maintainability, scalability, and performance. Discover how these patterns equip businesses with cleaner and more maintainable code, promote team collaboration, reduce errors, and save time and costs. This book provides a comprehensive view of design patterns in modern (ES6+) JavaScript with real-world examples of their deployment in professional settings. You’ll start by learning how to use creational, structural, and behavioral design patterns in idiomatic JavaScript, and then shift focus to the architecture and UI patterns. Here, you’ll learn how to apply patterns for libraries such as React and extend them further to general web frontend and micro frontend approaches. The last section of the book introduces and illustrates sets of performance and security patterns, including messaging and events, asset and JavaScript loading strategies, and asynchronous programming performance patterns. Throughout the book, examples featuring React and Next.js, in addition to JavaScript and Web API examples, will help you choose and implement proven design patterns across diverse web ecosystems, transforming the way you approach development.
Publication date:
March 2024
Publisher
Packt
Pages
308
ISBN
9781804612279

 

Working with Creational Design Patterns

JavaScript design patterns are techniques that allow us to write more robust, scalable, and extensible applications in JavaScript. JavaScript is a very popular programming language, in part due to its place as a way to deliver interactive functionality on web pages. The other reason for its popularity is JavaScript’s lightweight, dynamic, multi-paradigm nature, which means that design patterns from other ecosystems can be adapted to take advantage of JavaScript’s strengths. JavaScript’s specific strengths and weaknesses can also inform new patterns specific to the language and the contexts in which it’s used.

Creational design patterns give structure to object creation, which enables the development of systems and applications where different modules, classes, and objects don’t need to know how to create instances of each other. The design patterns most relevant to JavaScript – the prototype, singleton, and factory patterns – will be explored, as well as situations where they’re helpful and how to implement them in an idiomatic fashion.

We’ll cover the following topics in this chapter:

  • A comprehensive definition of creational design patterns and definitions for the prototype, singleton, and factory patterns
  • Multiple implementations of the prototype pattern and its use cases
  • An implementation of the singleton design pattern, eager and lazy initialization, use cases for singleton, and what a singleton pattern in modern JavaScript looks like
  • How to implement the factory pattern using classes, a modern JavaScript alternative, and use cases

By the end of this chapter, you’ll be able to identify when a creational design pattern is useful and make an informed decision on which of its multiple implementations to use, ranging from a more idiomatic JavaScript form to a classical form.

 

What are creational design patterns?

Creational design patterns handle object creation. They allow a consumer to create object instances without knowing the details of how to instantiate the object. Since, in object-oriented languages, instantiation of objects is limited to a class’s constructor, allowing object instances to be created without calling the constructor is useful to reduce noise and tight coupling between the consumer and the class being instantiated.

In JavaScript, there’s ambiguity when we discuss “object creation,” since JavaScript’s multi-paradigm nature means we can create objects without a class or a constructor. For example, in JavaScript this is an object creation using an object literal – const config = { forceUpdate: true }. In fact, modern idiomatic JavaScript tends to lean more toward procedural and function paradigms than object orientation. This means that creational design patterns may have to be adapted to be fully useful in JavaScript.

In summary, creational design patterns are useful in object-oriented JavaScript, since they hide instantiation details from consumers, which keeps coupling low, thereby allowing better module separation.

In the next section, we’ll encounter our first creational design pattern – the prototype design pattern.

 

Implementing the prototype pattern in JavaScript

Let’s start with a definition of the prototype pattern first.

The prototype design pattern allows us to create an instance based on another existing instance (our prototype).

In more formal terms, a prototype class exposes a clone() method. Consuming code, instead of calling new SomeClass, will call new SomeClassPrototype(someClassInstance).clone(). This method call will return a new SomeClass instance with all the values copied from someClassInstance.

Implementation

Let’s imagine a scenario where we’re building a chessboard. There are two key types of squares – white and black. In addition to this information, each square contains information such as its row, file, and which piece sits atop it.

A BoardSquare class constructor might look like the following:

class BoardSquare {
  constructor(color, row, file, startingPiece) {
    this.color = color;
    this.row = row;
    this.file = file;
  }
}

A set of useful methods on BoardSquare might be occupySquare and clearSquare, as follows:

class BoardSquare {
  // no change to the rest of the class
  occupySquare(piece) {
    this.piece = piece;
  }
  clearSquare() {
    this.piece = null;
  }
}

Instantiating BoardSquare is quite cumbersome, due to all its properties:

const whiteSquare = new BoardSquare('white');
const whiteSquareTwo = new BoardSquare('white');
// ...
const whiteSquareLast = new BoardSquare('white');

Note the repetition of arguments being passed to new BoardSquare, which will cause issues if we want to change all board squares to black. We would need to change the parameter passed to each call of BoardSquare is one by one for each new BoardSquare call. This can be quite error-prone; all it takes is one hard-to-find mistake in the color value to cause a bug:

const blackSquare = new BoardSquare('black');
const blackSquareTwo = new BoardSquare('black');
// ...
const blackSquareLast = new BoardSquare('black');

Implementing our instantiation logic using a classical prototype looks as follows. We need a BoardSquarePrototype class; its constructor takes a prototype property, which it stores on the instance. BoardSquarePrototype exposes a clone() method that takes no arguments and returns a BoardSquare instance, with all the properties of prototype copied onto it:

class BoardSquarePrototype {
  constructor(prototype) {
    this.prototype = prototype;
  }
  clone() {
    const boardSquare = new BoardSquare();
    boardSquare.color = this.prototype.color;
    boardSquare.row = this.prototype.row;
    boardSquare.file = this.prototype.file;
    return boardSquare;
  }
}

Using BoardSquarePrototype requires the following steps:

  1. First, we want an instance of BoardSquare to initialize – in this case, with 'white'. It will then be passed as the prototype property during the BoardSquarePrototype constructor call:
    const whiteSquare = new BoardSquare('white');
    const whiteSquarePrototype = new BoardSquarePrototype
      (whiteSquare);
  2. We can then use whiteSquarePrototype with .clone() to create our copies of whiteSquare. Note that color is copied over but each call to clone() returns a new instance.
    const whiteSquareTwo = whiteSquarePrototype.clone();
    // ...
    const whiteSquareLast = whiteSquarePrototype.clone();
    console.assert(
      whiteSquare.color === whiteSquareTwo.color &&
        whiteSquareTwo.color === whiteSquareLast.color,
      'Prototype.clone()-ed instances have the same color
         as the prototype'
    );
    console.assert(
      whiteSquare !== whiteSquareTwo &&
        whiteSquare !== whiteSquareLast &&
        whiteSquareTwo !== whiteSquareLast,
      'each Prototype.clone() call outputs a different
         instances'
    );

Per the assertions in the code, the cloned instances contain the same value for color but are different instances of the Square object.

A use case

To illustrate what it would take to change from a white square to a black square, let’s look at some sample code where 'white' is not referenced in the variable names:

const boardSquare = new BoardSquare('white');
const boardSquarePrototype = new BoardSquarePrototype(boardSquare);
const boardSquareTwo = boardSquarePrototype.clone();
// ...
const boardSquareLast = boardSquarePrototype.clone();
console.assert(
  boardSquareTwo.color === 'white' &&
    boardSquare.color === boardSquareTwo.color &&
    boardSquareTwo.color === boardSquareLast.color,
  'Prototype.clone()-ed instances have the same color as
     the prototype'
);
console.assert(
  boardSquare !== boardSquareTwo &&
    boardSquare !== boardSquareLast &&
    boardSquareTwo !== boardSquareLast,
  'each Prototype.clone() call outputs a different
    instances'
);

In this scenario, we would only have to change the color value passed to BoardSquare to change the color of all the instances cloned from the prototype:

const boardSquare = new BoardSquare('black');
// rest of the code stays the same
console.assert(
  boardSquareTwo.color === 'black' &&
    boardSquare.color === boardSquareTwo.color &&
    boardSquareTwo.color === boardSquareLast.color,
  'Prototype.clone()-ed instances have the same color as
     the prototype'
);
console.assert(
  boardSquare !== boardSquareTwo &&
    boardSquare !== boardSquareLast &&
    boardSquareTwo !== boardSquareLast,
  'each Prototype.clone() call outputs a different
     instances'
);

The prototype pattern is useful in situations where a “template” for the object instances is useful. It’s a good pattern to create a “default object” but with custom values. It allows faster and easier changes, since they are implemented once on the template object but are applied to all clone()-ed instances.

Increasing robustness to change in the prototype’s instance variables with modern JavaScript

There are improvements we can make to our prototype implementation in JavaScript.

The first is in the clone() method. To make our prototype class robust to changes in the prototype’s constructor/instance variables, we should avoid copying the properties one by one.

For example, if we add a new startingPiece parameter that the BoardSquare constructor takes and sets the piece instance variable to, our current implementation of BoardSquarePrototype will fail to copy it, since it only copies color, row, and file:

class BoardSquare {
  constructor(color, row, file, startingPiece) {
    this.color = color;
    this.row = row;
    this.file = file;
    this.piece = startingPiece;
  }
  // same rest of the class
}
const boardSquare = new BoardSquare('white', 1, 'A',
  'king');
const boardSquarePrototype = new BoardSquarePrototype
  (boardSquare);
const otherBoardSquare = boardSquarePrototype.clone();
console.assert(
  otherBoardSquare.piece === undefined,
  'prototype.piece was not copied over'
);

Note

Reference for Object.assign: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign.

If we amend our BoardSquarePrototype class to use Object.assign(new BoardSquare(), this.prototype), it will copy all the enumerable properties of prototype:

class BoardSquarePrototype {
  constructor(prototype) {
    this.prototype = prototype;
  }
  clone() {
    return Object.assign(new BoardSquare(), this.prototype);
  }
}
const boardSquare = new BoardSquare('white', 1, 'A',
  'king');
const boardSquarePrototype = new BoardSquarePrototype
  (boardSquare);
const otherBoardSquare = boardSquarePrototype.clone();
console.assert(
  otherBoardSquare.piece === 'king' &&
    otherBoardSquare.piece === boardSquare.piece,
  'prototype.piece was copied over'
);

The prototype pattern without classes in JavaScript

For historical reasons, JavaScript has a prototype concept deeply embedded into the language. In fact, classes were introduced much later into the ECMAScript standard, with ECMAScript 6, which was released in 2015 (for reference, ECMAScript 1 was published in 1997).

This is why a lot of JavaScript completely forgoes the use of classes. The JavaScript “object prototype” can be used to make objects inherit methods and variables from each other.

One way to clone objects is by using the Object.create to clone objects with their methods. This relies on the JavaScript prototype system:

const square = {
  color: 'white',
  occupySquare(piece) {
    this.piece = piece;
  },
  clearSquare() {
    this.piece = null;
  },
};
const otherSquare = Object.create(square);

One subtlety here is that Object.create does not actually copy anything; it simply creates a new object and sets its prototype to square. This means that if properties are not found on otherSquare, they’re accessed on square:

console.assert(otherSquare.__proto__ === square, 'uses JS
  prototype');
console.assert(
  otherSquare.occupySquare === square.occupySquare &&
    otherSquare.clearSquare === square.clearSquare,
  "methods are not copied, they're 'inherited' using the
     prototype"
);
delete otherSquare.color;
console.assert(
  otherSquare.color === 'white' && otherSquare.color ===
    square.color,
  'data fields are also inherited'
);

A further note on the JavaScript prototype, and its existence before classes were part of JavaScript, is that subclassing in JavaScript is another syntax for setting an object’s prototype. Have a look at the following extends example. BlackSquare extends Square sets the prototype.__proto__ property of BlackSquare to Square.prototype:

class Square {
  constructor() {}
  occupySquare(piece) {
    this.piece = piece;
  }
  clearSquare() {
    this.piece = null;
  }
}
class BlackSquare extends Square {
  constructor() {
    super();
    this.color = 'black';
  }
}
console.assert(
  BlackSquare.prototype.__proto__ === Square.prototype,
  'subclass prototype has prototype of superclass'
);

In this section, we learned how to implement the prototype pattern with a prototype class that exposes a clone() method, which code situations the prototype patterns can help with, and how to further improve our prototype implementation with modern JavaScript features. We also covered the JavaScript “prototype,” why it exists, and its relationship with the prototype design pattern.

In the next part of the chapter, we’ll look at another creational design pattern, the singleton design pattern, with some implementation approaches in JavaScript and its use cases.

 

The singleton pattern with eager and lazy initialization in JavaScript

To begin, let’s define the singleton design pattern.

The singleton pattern allows an object to be instantiated only once, exposes this single instance to consumers, and controls the instantiation of the single instance.

The singleton is another way of getting access to an object instance without using a constructor, although it’s necessary for the object to be designed as a singleton.

Implementation

A classic example of a singleton is a logger. It’s rarely necessary (and often, it’s a problem) to instantiate multiple loggers in an application. Having a singleton means the initialization site is controlled, and the logger configuration will be consistent across the application – for example, the log level won’t change depending on where in the application we call the logger from.

A simple logger looks something as follows, with a constructor taking logLevel and transport, and an isLevelEnabled private method, which allows us to drop logs that the logger is not configured to keep (for example, when the level is warn we drop info messages). The logger finally implements the info, warn, and error methods, which behave as previously described; they only call the relevant transport method if the level is “enabled” (i.e., “above” what the configured log level is).

The possible logLevel values that power isLevelEnabled are stored as a static field on Logger:

class Logger {
  static logLevels = ['info', 'warn', 'error'];
  constructor(logLevel = 'info', transport = console) {
    if (Logger.#loggerInstance) {
      throw new TypeError(
        'Logger is not constructable, use getInstance()
           instead'
      );
    }
    this.logLevel = logLevel;
    this.transport = transport;
  }
  isLevelEnabled(targetLevel) {
    return (
      Logger.logLevels.indexOf(targetLevel) >=
      Logger.logLevels.indexOf(this.logLevel)
    );
  }
  info(message) {
    if (this.isLevelEnabled('info')) {
      return this.transport.info(message);
    }
  }
  warn(message) {
    if (this.isLevelEnabled('warn')) {
      this.transport.warn(message);
    }
  }
  error(message) {
    if (this.isLevelEnabled('error')) {
      this.transport.error(message);
    }
  }
}

In order to make Logger a singleton, we need to implement a getInstance static method that returns a cached instance. In order to do, this we’ll use a static loggerInstance on Logger. getInstance will check whether Logger.loggerInstance exists and return it if it does; otherwise, it will create a new Logger instance, set that as loggerInstance, and return it:

class Logger {
  static loggerInstance = null;
  // rest of the class
  static getInstance() {
    if (!Logger.loggerInstance) {
      Logger.loggerInstance = new Logger('warn', console);
    }
    return Logger.loggerInstance;
  }
}

Using this in another module is as simple as calling Logger.getInstance(). All getInstance calls will return the same instance of Logger:

const a = Logger.getInstance();
const b = Logger.getInstance();
console.assert(a === b, 'Logger.getInstance() returns the
  same reference');

We’ve implemented a singleton with “lazy” initialization. The initialization occurs when the first getInstance call is made. In the next section, we’ll see how we might extend our code to have an “eager” initialization of loggerInstance, where loggerInstance will be initialized when the Logger code is evaluated.

Ensuring only one singleton instance is constructed

A characteristic of a singleton is the “single instance” concept. We want to “force” consumers to use the getInstance method.

In order to do this, we can check for the existence of loggerInstance when the contructor is called:

class Logger {
  // rest of the class
  constructor(logLevel = 'info', transport = console) {
    if (Logger.loggerInstance) {
      throw new TypeError(
        'Logger is not constructable, use getInstance()
          instead'
      );
    }
    this.logLevel = logLevel;
    this.transport = transport;
  }
  // rest of the class
}

In the case where we call getInstance (and, therefore, Logger.loggerInstance is populated), the constructor will now throw an error:

Logger.getInstance();
new Logger('info', console); // new TypeError('Logger is
  not constructable, use getInstance() instead');

This behavior is useful to ensure that consumers don’t instantiate their own Logger and they use getInstance instead. All consumers using getInstance means the configuration to set up the logger is encapsulated by the Logger class.

There’s still a gap in the implementation, as constructing new Logger() before any getInstance() calls will succeed, as shown in the following example:

new Logger('info', console); // Logger { logLevel: 'info',
  transport: ... }
new Logger('info', console); // Logger { logLevel: 'info',
  transport: ... }
Logger.getInstance();
new Logger('info', console); // new TypeError('Logger is
  not constructable, use getInstance() instead');

In multithreaded languages, our implementation would also have a potential race condition – multiple consumers calling Logger.getInstance() concurrently could cause multiple instances to exist. However, since popular JavaScript runtimes are single-threaded, we won’t have to worry about such a race condition – getInstance is a “synchronous” method, so multiple calls to it would be interpreted one after the other. For reference, Node.js, Deno, and the mainstream browsers Chrome, Safari, Edge, and Firefox provide a single-threaded JavaScript runtime.

Singleton with eager initialization

Eager initialization can be useful to ensure that the singleton is ready for use and features, such as disabling the constructor when an instance exists, work for all cases.

We can eager-initialize by setting Logger.loggerInstance in the Logger constructor:

class Logger {
  // rest of the class unchanged
  constructor(logLevel = 'info', transport = console) {
    // rest of the constructor unchanged
    Logger.loggerInstance = this;
  }
}

This approach has the downside of the constructor performing a global state mutation, which isn’t ideal from a “single responsibility principle” standpoint; the constructor now has a side-effect of sorts (mutating global state) beyond its responsibility to set up an object instance.

An alternative way to eager-initialize is by running Logger.getInstance() in the logger’s module; it’s useful to pair it with an export default statement:

export class Logger {
  // no changes to the Logger class
}
export default Logger.getInstance();

With the preceding exports added, there are now two ways to access a logger instance. The first is to import Logger by name and call Logger.getInstance():

import { Logger } from './logger.js';
const logger = Logger.getInstance();
logger.warn('testing testing 12'); // testing testing 12

The second way to use the logger is by importing the default export:

import logger from './logger.js';
logger.warn('testing testing 12'); // testing testing 12

Any code now importing Logger will get a pre-determined singleton instance of the logger.

Use cases

A singleton shines when there should only be one instance of an object in an application – for example, a logger that shouldn’t be set up/torn down on every request.

Since the singleton class controls how it gets instantiated, it’s also a good fit for objects that are tricky to configure (again, a logger, a metrics exporter, and an API client are good examples). The instantiation is completely encapsulated if, like in our example, we “disable” the constructor.

There’s a performance benefit to constraining the application to a single instance of an object in terms of memory footprint.

The major drawbacks of singletons are an effect of their reliance on global state (in our example, the static loggerInstance). It’s hard to test a singleton, especially in a case where the constructor is “disabled” (like in our example), since our tests will want to always have a single instance of the singleton.

Singletons can also be considered “global state” to some extent, which comes with all its drawbacks. Global state can sometimes be a sign of poor design, and updating/consuming global state is error-prone (e.g., if a consumer is reading state but it is then updated and not read again).

Improvements with the “class singleton” pattern

With our singleton logger implementation, it’s possible to modify the internal state of the singleton from outside of it. This is nothing specific to our singleton; it’s the nature of JavaScript. By default, its fields and methods are public.

However, this is a bigger issue in our singleton scenario, since a consumer could reset loggerInstance using a statement such as Logger.loggerInstance = null or delete Logger.loggerInstance. See the following example:

const logger = Logger.getInstance();
Logger.loggerInstance = null;
const logger = new Logger('info', console); // should throw but creates a new instance

In order to stop consumers from modifying the loggerInstance static field, we can make it a private field. Private fields in JavaScript are part of the ECMAScript 2023 specification (the 13th ECMAScript edition).

To define a private field, we use the # prefix for the field name – in this case, loggerInstance becomes #loggerInstance. The isLevelEnabled method becomes #isLevelEnabled, and we also declare logLevel and transport as #logLevel and #transport, respectively:

export class Logger {
  // other static fields are unchanged
  static #loggerInstance = null;
  #logLevel;
  #transport;
  constructor(logLevel = 'info', transport = console) {
    if (Logger.#loggerInstance) {
      throw new TypeError(
        'Logger is not constructable, use getInstance()
          instead'
      );
    }
    this.#logLevel = logLevel;
    this.#transport = transport;
  }
  #isLevelEnabled(targetLevel) {
    // implementation unchanged
  }
  info(message) {
    if (this.#isLevelEnabled('info')) {
      return this.#transport.info(message);
    }
  }
  warn(message) {
    if (this.#isLevelEnabled('warn')) {
      this.#transport.warn(message);
    }
  }
  error(message) {
    if (this.#isLevelEnabled('error')) {
      this.#transport.error(message);
    }
  }
  getInstance() {
    if (!Logger.#loggerInstance) {
      Logger.#loggerInstance = new Logger('warn', console);
    }
    return Logger.#loggerInstance;
  }
}

It’s not possible to delete loggerInstace or set it to null, since attempting to access Logger.#loggerInstance is a syntax error:

  Logger.#loggerInstance = null;
        ^
SyntaxError: Private field '#loggerInstance' must be
  declared in an enclosing class

Another useful technique is to disallow modification of fields on an object. In order to disallow modification, we can use Object.freeze to freeze the instance once it’s created.

class Logger {
  // no changes to the logger class
}
export default Object.freeze(new Logger('warn', console));

Now, when someone attempts to change a field on the Logger instance, they’ll get TypeError:

import logger from './logger.js';
logger.transport = {}; // new TypeError('Cannot add
  property transport, object is not extensible')

We’ve now refactored our singleton implementation to disallow external modifications to it by using private fields and Object.freeze. Next, we’ll see how to use EcmaScript (ES) modules to deliver singleton functionality.

A singleton without class fields using ES module behavior

The JavaScript module system has the following caching behavior – if a module is loaded, any further imports of the module’s exports will be cached instances of exports.

Therefore, it’s possible to create a singleton as follows in JavaScript.

class MySingleton {
  constructor(value) {
    this.value = value;
  }
}
export default new MySingleton('my-value');

Multiple imports of the default export will result in only one existing instance of the MySingleton object. Furthermore, if we don’t export the class, then the constructor doesn’t need to be “protected.”

As the following snippet with dynamic imports shows, both import('./my-singleton.js') result in the same object. They both return the same object because the output of the import for a given module is a singleton:

await Promise.all([
  import('./my-singleton.js'),
  import('./my-singleton.js'),
]).then(([import1, import2]) => {
  console.assert(
    import1.default.value === 'my-value' &&
      import2.default.value === 'my-value',
    'instance variable is equal'
  );
  console.assert(
    import1.default === import2.default,
    'multiple imports of a module yield the same default
      object value, a single MySingleton instance'
  );
  console.assert(import1 === import2, 'import objects are a
    single reference');
});

For our logger, this means we could implement an eager-initialized singleton in JavaScript without any of the heavy-handed guarding of the constructor or even a getInstance method. Note the use of logLevel and isLevelEnabled as a public instance property and a public method, respectively (since it might be useful to have access to them from a consumer). In the meantime, #transport remains private, and we’ve dropped loggerInstance and getInstance. We’ve kept Object.freeze(), which means that even though logLevel is readable from a consumer, it’s not available to modify:

class Logger {
  static logLevels = ['info', 'warn', 'error'];
  #transport;
  constructor(logLevel = 'info', transport = console) {
    this.logLevel = logLevel;
    this.#transport = transport;
  }
  isLevelEnabled(targetLevel) {
    return (
      Logger.logLevels.indexOf(targetLevel) >=
      Logger.logLevels.indexOf(this.logLevel)
    );
  }
  info(message) {
    if (this.isLevelEnabled('info')) {
      return this.#transport.info(message);
    }
  }
  warn(message) {
    if (this.isLevelEnabled('warn')) {
      this.#transport.warn(message);
    }
  }
  error(message) {
    if (this.isLevelEnabled('error')) {
      this.#transport.error(message);
    }
  }
}
export default Object.freeze(new Logger('warn', console));

In this part of the chapter, we learned how to implement the singleton pattern with a class that exposes a getInstance() method, as well as the difference between the eager and lazy initialization of a singleton. We’ve covered some JavaScript features, such as private class fields and Object.freeze, which can be useful when implementing the singleton pattern. Finally, we explored how JavaScript/ECMAScript modules have singleton-like behavior and can be relied upon to provide this behavior for a class instance.

In the next section, we’ll explore the final creational design pattern covered in this chapter – the factory design pattern.

 

The factory pattern in JavaScript

In a similar fashion to the discussion about the JavaScript “prototype” versus the prototype creational design pattern, “factory” refers to related but different concepts when it comes to general program design discussions and design patterns.

A “factory,” in the general programming sense, is an object that’s built with the goal of creating other objects. This is hinted at by the name that refers to a facility that processes items from one shape into another (or from one type of item to another). This factory denomination means that the output of a function or method is a new object. In JavaScript, this means that something as simple as a function that returns an object literal is a factory function:

const simpleFactoryFunction = () => ({}); // returns an object, therefore it's a factory.

This definition of a factory is useful, but this section of the chapter is about the factory design pattern, which does fit into this overall “factory” definition.

The factory or factory method design pattern solves a class inheritance problem. A base or superclass is extended (the extended class is a subclass). The base class’s role is to provide orchestration for the methods implemented in the subclasses, as we want the subclasses to control which other objects to populate an instance with.

Implementation

A factory example is as follows. We have a Building base class that implements a generateBuilding() method. For now, it’s going to create a top floor using the makeTopFloor instance method. In the base class (Building), makeTopFloor is implemented, mainly because JavaScript doesn’t provide a way to define abstract methods. The makeTopFloor implementation throws an error because subclasses should override it; makeTopFloor is the “factory method” in this case. It’s how the base class defers the instantiation of objects to the subclasses:

class Building {
  generateBuilding() {
    this.topFloor = this.makeTopFloor();
  }
  makeTopFloor() {
    throw new Error('not implemented, left for subclasses
      to implement');
  }
}

If we wanted to implement a single-story house, we would extend Building and override makeTopFloor; in this instance, topFloor will have level: 1.

class House extends Building {
  makeTopFloor() {
    return {
      level: 1,
    };
  }
}

When we instantiate House, which is a subclass of Building, we have access to the generateBuilding method; when called, it sets topFloor correctly (to { level: 1 }).

const house = new House();
house.generateBuilding();
console.assert(house.topFloor.level === 1, 'topFloor works
  in House');

Now, if we want to create a different type of building that has a very different top floor, we can still extend Building; we simply override makeTopFloor to return a different floor. In the case of a skyscraper, we want the top floor to be very high, so we’ll do the following:

class SkyScraper extends Building {
  makeTopFloor() {
    return {
      level: 125,
    };
  }
}

Having defined our SkyScraper, which is a subclass of Building, we can instantiate it and call generateBuilding. As in the preceding House case, the generateBuilding method will use SkyScraper’s makeTopFloor method to populate the topFloor instance property:

const skyScraper = new SkyScraper();
skyScraper.generateBuilding();
console.assert(skyScraper.topFloor.level > 100, 'topFloor
  works in SkyScraper');

The “factory method” in this case is makeTopFloor. The makeTopFloor method is “not implemented” in the base class, in the sense that it’s implemented in a manner that forces subclasses that wish to use generateBuilding to define a makeTopFloor override.

Note that makeTopFloor in our examples returned object literals, as mentioned earlier in the chapter; this is a feature of JavaScript not available in all object-oriented languages (JavaScript is multi-paradigm). We’ll see different ways to implement the factory pattern later in this section.

Use cases

The benefit of using a factory method is that we can create a wide variety of subclasses without modifying the base class. This is the “open/closed principle” at play – the Building class in our example is “open” to extension (i.e., can be subclassed to infinity for different types of buildings) but “closed” to modification (i.e., we don’t need to make changes in Building for every subclass, only when we want to add new behaviors).

Improvements with modern JavaScript

The key improvement we can make with JavaScript is enabled by its first-class support for functions and the ability to define objects using literals (instead of classes being instantiated).

JavaScript having “first-class functions” means functions are like any other type – they can be passed as parameters, set as variable values, and returned from other functions.

A more idiomatic implementation of this pattern would probably involve a generateBuilding standalone function instead of a Building class. generateBuilding would take makeTopFloor either as a parameter or take an object parameter with a makeTopFloor key. The output of generateBuilding would be an object created using an object literal, which takes the output of makeTopFloor() and sets it as the value to a topFloor key:

function generateBuilding({ makeTopFloor }) {
  return {
    topFloor: makeTopFloor(),
  };
}

In order to create our house and skyscraper, we would call generateBuilding with the relevant makeTopFloor functions. In the case of the house, we want a top floor that is on level 1; in the case of the skyscraper, we want a top floor on level 125.

const house = generateBuilding({
  makeTopFloor() {
    return {
      level: 1,
    };
  },
});
console.assert(house.topFloor.level === 1, 'topFloor works
  in house');
const skyScraper = generateBuilding({
  makeTopFloor() {
    return {
      level: 125,
    };
  },
});
console.assert(skyScraper.topFloor.level > 100, 'topFloor works in skyScraper');

One reason why using functions directly works better in JavaScript is that we didn’t have to implement a “throw an error to remind consumers to override me” makeFloor method that we had with the Building class.

In languages other than JavaScript that have support for abstract methods, this pattern is more useful and natural to implement than in JavaScript, where we have first-class functions.

You also have to bear in mind that the original versions of JavaScript/ECMAScript didn’t include a class construct.

In the final section of the chapter, we learned what the factory method pattern is and how it contrasts with the factory programming concept. We then implemented a class-based factory pattern scenario as well as a more idiomatic JavaScript version. Interspersed through this section, we covered the use cases, benefits, and drawbacks of the factory method pattern in JavaScript.

 

Summary

Throughout this chapter, we discussed how creational design patterns allow us to build more extensible and maintainable systems in JavaScript.

The prototype design pattern shines when creating many instances of objects that contain the same values. This design pattern allows us to change the initial values of the prototype and affect all the cloned instances.

The singleton design pattern is useful to completely hide initialization details of a class that should really only be instantiated once. We saw how JavaScript’s module system generates singletons and how that can be leveraged to simplify a singleton implementation.

The factory method design pattern allows a base class to defer the implementation of some object creations to subclasses. We saw which features would make this pattern more useful in JavaScript, as well as an alternative idiomatic JavaScript approach with factory functions.

We can now leverage creational design patterns to build classes that are composable and can be evolved as necessary to cover different use cases.

Now that we know how to create objects efficiently with creational design patterns, in the next chapter, we’ll cover how to use structural design patterns to organize relationships between different objects and classes.

About the Author
  • Hugo Di Francesco

    Hugo Di Francesco is a software engineer who has worked extensively with JavaScript. He holds a MEng degree in mathematical computation from University College London (UCL). He has used JavaScript across the stack to create scalable and performant platforms at companies such as Canon and Elsevier and in industries such as print on demand and mindfulness. He is currently tackling problems in the travel industry at Eurostar with Node.js, TypeScript, React, and Kubernetes while running the eponymous Code with Hugo website. Outside of work, he is an international fencer, in the pursuit of which he trains and competes across the globe.

    Browse publications by this author
JavaScript Design Patterns
Unlock this book and the full library FREE for 7 days
Start now