Search icon
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletters
Free Learning
Arrow right icon
JavaScript Design Patterns
JavaScript Design Patterns

JavaScript Design Patterns: Deliver fast and efficient production-grade JavaScript applications at scale

By Hugo Di Francesco
$36.99
Book Mar 2024 308 pages 1st Edition
eBook
$29.99 $20.98
Print
$36.99
Subscription
$15.99 Monthly
eBook
$29.99 $20.98
Print
$36.99
Subscription
$15.99 Monthly

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Black & white paperback book shipped to your address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Buy Now

Product Details


Publication date : Mar 15, 2024
Length 308 pages
Edition : 1st Edition
Language : English
ISBN-13 : 9781804612279
Category :
Table of content icon View table of contents Preview book icon Preview Book

JavaScript Design Patterns

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.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Explore various JavaScript design patterns, delving deep into their intricacies, benefits, and best practices
  • Understand the decision-making process guiding the selection of specific design patterns
  • Build a solid foundation to learn advanced topics in JavaScript and web performance
  • Purchase of the print or Kindle book includes a free PDF eBook

Description

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.

What you will learn

Find out how patterns are classified into creational, structural, and behavioral Implement the right set of patterns for different business scenarios Explore diverse frontend architectures and different rendering approaches Identify and address common asynchronous programming performance pitfalls Leverage event-driven programming in the browser to deliver fast and secure applications Boost application performance using asset loading strategies and offloading JavaScript execution

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Black & white paperback book shipped to your address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Buy Now

Product Details


Publication date : Mar 15, 2024
Length 308 pages
Edition : 1st Edition
Language : English
ISBN-13 : 9781804612279
Category :

Table of Contents

16 Chapters
Preface Chevron down icon Chevron up icon
Part 1:Design Patterns Chevron down icon Chevron up icon
Chapter 1: Working with Creational Design Patterns Chevron down icon Chevron up icon
Chapter 2: Implementing Structural Design Patterns Chevron down icon Chevron up icon
Chapter 3: Leveraging Behavioral Design Patterns Chevron down icon Chevron up icon
Part 2:Architecture and UI Patterns Chevron down icon Chevron up icon
Chapter 4: Exploring Reactive View Library Patterns Chevron down icon Chevron up icon
Chapter 5: Rendering Strategies and Page Hydration Chevron down icon Chevron up icon
Chapter 6: Micro Frontends, Zones, and Islands Architectures Chevron down icon Chevron up icon
Part 3:Performance and Security Patterns Chevron down icon Chevron up icon
Chapter 7: Asynchronous Programming Performance Patterns Chevron down icon Chevron up icon
Chapter 8: Event-Driven Programming Patterns Chevron down icon Chevron up icon
Chapter 9: Maximizing Performance – Lazy Loading and Code Splitting Chevron down icon Chevron up icon
Chapter 10: Asset Loading Strategies and Executing Code off the Main Thread Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Filter icon Filter
Top Reviews
Rating distribution
Empty star icon Empty star icon Empty star icon Empty star icon Empty star icon 0
(0 Ratings)
5 star 0%
4 star 0%
3 star 0%
2 star 0%
1 star 0%

Filter reviews by


No reviews found
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

What is the delivery time and cost of print book? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela
What is custom duty/charge? Chevron down icon Chevron up icon

Customs duty are charges levied on goods when they cross international borders. It is a tax that is imposed on imported goods. These duties are charged by special authorities and bodies created by local governments and are meant to protect local industries, economies, and businesses.

Do I have to pay customs charges for the print book order? Chevron down icon Chevron up icon

The orders shipped to the countries that are listed under EU27 will not bear custom charges. They are paid by Packt as part of the order.

List of EU27 countries: www.gov.uk/eu-eea:

A custom duty or localized taxes may be applicable on the shipment and would be charged by the recipient country outside of the EU27 which should be paid by the customer and these duties are not included in the shipping charges been charged on the order.

How do I know my custom duty charges? Chevron down icon Chevron up icon

The amount of duty payable varies greatly depending on the imported goods, the country of origin and several other factors like the total invoice amount or dimensions like weight, and other such criteria applicable in your country.

For example:

  • If you live in Mexico, and the declared value of your ordered items is over $ 50, for you to receive a package, you will have to pay additional import tax of 19% which will be $ 9.50 to the courier service.
  • Whereas if you live in Turkey, and the declared value of your ordered items is over € 22, for you to receive a package, you will have to pay additional import tax of 18% which will be € 3.96 to the courier service.
How can I cancel my order? Chevron down icon Chevron up icon

Cancellation Policy for Published Printed Books:

You can cancel any order within 1 hour of placing the order. Simply contact customercare@packt.com with your order details or payment transaction id. If your order has already started the shipment process, we will do our best to stop it. However, if it is already on the way to you then when you receive it, you can contact us at customercare@packt.com using the returns and refund process.

Please understand that Packt Publishing cannot provide refunds or cancel any order except for the cases described in our Return Policy (i.e. Packt Publishing agrees to replace your printed book because it arrives damaged or material defect in book), Packt Publishing will not accept returns.

What is your returns and refunds policy? Chevron down icon Chevron up icon

Return Policy:

We want you to be happy with your purchase from Packtpub.com. We will not hassle you with returning print books to us. If the print book you receive from us is incorrect, damaged, doesn't work or is unacceptably late, please contact Customer Relations Team on customercare@packt.com with the order number and issue details as explained below:

  1. If you ordered (eBook, Video or Print Book) incorrectly or accidentally, please contact Customer Relations Team on customercare@packt.com within one hour of placing the order and we will replace/refund you the item cost.
  2. Sadly, if your eBook or Video file is faulty or a fault occurs during the eBook or Video being made available to you, i.e. during download then you should contact Customer Relations Team within 14 days of purchase on customercare@packt.com who will be able to resolve this issue for you.
  3. You will have a choice of replacement or refund of the problem items.(damaged, defective or incorrect)
  4. Once Customer Care Team confirms that you will be refunded, you should receive the refund within 10 to 12 working days.
  5. If you are only requesting a refund of one book from a multiple order, then we will refund you the appropriate single item.
  6. Where the items were shipped under a free shipping offer, there will be no shipping costs to refund.

On the off chance your printed book arrives damaged, with book material defect, contact our Customer Relation Team on customercare@packt.com within 14 days of receipt of the book with appropriate evidence of damage and we will work with you to secure a replacement copy, if necessary. Please note that each printed book you order from us is individually made by Packt's professional book-printing partner which is on a print-on-demand basis.

What tax is charged? Chevron down icon Chevron up icon

Currently, no tax is charged on the purchase of any print book (subject to change based on the laws and regulations). A localized VAT fee is charged only to our European and UK customers on eBooks, Video and subscriptions that they buy. GST is charged to Indian customers for eBooks and video purchases.

What payment methods can I use? Chevron down icon Chevron up icon

You can pay with the following card types:

  1. Visa Debit
  2. Visa Credit
  3. MasterCard
  4. PayPal
What is the delivery time and cost of print books? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela