DEV Community

Manoj Kumar Patra
Manoj Kumar Patra

Posted on

Creational Design Patterns

# Factory

Ability to decouple the creation of an object from one particular implementation.

Inside the factory, we can choose to create a new instance of a class using the new operator, or leverage closures to dynamically build a stateful object literal, or even return a different object type based on a particular condition.

A factory can also be used as an encapsulation mechanism, thanks to closures.

⭐ Encapsulation refers to controlling the access to some internal details of a component by preventing external code from manipulating them directly.

EXAMPLE 1


function createPerson (name) {
  const privateProperties = {};
  const person = {
    setName (name) {
      if (!name) {
        throw new Error('A person must have a name');
      }
      privateProperties.name = name;
    }
    getName () {
      return privateProperties.name;
    }
  };
  person.setName(name);
  return person;
}

Enter fullscreen mode Exit fullscreen mode

Different ways to enforce encapsulation:

  1. Using closures
  2. πŸ”— Using private class fields (the hashbang # prefix syntax)
  3. πŸ”— Using WeakMaps
  4. πŸ”— Using symbols
  5. πŸ”— Defining private variables in a constructor

EXAMPLE 2


class Profiler {
  constructor(label) {
    this.label = label;
    this.lastTime = null;
  }

  start() {
    /**
     * process.hrtime() is used to measure performance 
     * in a more precise manner than Date.now(), 
     * as it provides timestamps in nanoseconds.
     * 
     * The returned value is an array with two elements:
     * The first element is the number of seconds.
     * The second element is the number of nanoseconds.
     */
    this.lastTime = process.hrtime();
  }

  end() {
    const diff = process.hrtime(this.lastTime);
    console.log(`Timer "${this.label}" took ${diff[0]} seconds ` +
      `and ${diff[1]} nanoseconds`);
  }
}

const noopProfiler = {
  start() {},
  end() {},
};

export function createProfiler(label) {
  if (process.env.NODE_ENV === 'production') {
    return noopProfiler;
  }

  return new Profiler(label);
}

Enter fullscreen mode Exit fullscreen mode

Examples of npm libraries using factory pattern - knex

# Builder

Builder is a creational design pattern that simplifies the creation of complex objects by providing a fluent interface, which allows us to build the object step by step.

EXAMPLE 1


class Boat {
  constructor(hasMotor, motorCount, motorBrand,
              motorModel, hasSails, sailsCount,
              sailsMaterial, sailsColor, hullColor,
              hasCabin) {}

  // ...
}

class BoatBuilder {
  withMotors(count, brand, model) {
    this.hasMotor = true;
    this.motorCount = count;
    this.motorBrand = brand;
    this.motorModel = model;
    return this;
  }

  withSails(count, material, color) {
    this.hasSails = true;
    this.sailsCount = count;
    this.sailsMaterial = material;
    this.sailsColor = color;
    return this;
  }

  hullColor(color) {
    this.hullColor = color;
    return this;
  }

  withCabin() {
    this.hasCabin = true;
    return this;
  }

  build() {
    return new Boat({
      hasMotor: true,
      motorCount: 2,
      motorBrand: 'Best Motor Co.',
      motorModel: 'OM123',
      hasSails: true,
      sailsCount: 1,
      sailsMaterial: 'fabric',
      sailsColor: 'white',
      hullColor: 'blue',
      hasCabin: false
    });
  }
}

const myBoat = new BoatBuilder()
  .withMotors(2, 'Best Motor Co.', 'OM123')
  .withSails(1, 'fabric', 'white')
  .withCabin()
  .hullColor('blue')
  .build();

Enter fullscreen mode Exit fullscreen mode

⭐ Using a builder that is separate from the target class has the advantage of always producing instances that are guaranteed to be in a consistent state.

EXAMPLE 2


export class Url {
  constructor (protocol, username, password, hostname,
    port, pathname, search, hash) {
    this.protocol = protocol;
    this.username = username;
    this.password = password;
    this.hostname = hostname;
    this.port = port;
    this.pathname = pathname;
    this.search = search;
    this.hash = hash;

    this.validate();
  }

  validate() {
    if (!this.protocol || !this.hostname) {
      throw new Error(`Must specify at least a protocol and a hostname`);
    }
  }

  toString() {
    let url = '';
    url += `${this.protocol}://`;
    if (this.username && this.password) {
      url += `${this.username}:${this.password}`;
    }
    url += this.hostname;
    if (this.port) {
      url += this.port;
    }
    if (this.pathname) {
      url += this.pathname;
    }
    if (this.search) {
      url += `?${this.search}`;
    }
    if (this.hash) {
      url += `#${this.hash}`;
    }
    return url;
  }
}

export class UrlBuilder {
  setProtocol(protocol) {
    this.protocol = protocol;
    return this;
  }

  setAuthentication(username, password) {
    this.username = username;
    this.password = password;
    return this;
  }

  setHostname(hostname) {
    this.hostname = hostname;
    return this;
  }

  setPort(port) {
    this.port = port;
    return this;
  }

  setPathname(pathname) {
    this.pathname = pathname;
    return this;
  }

  setSearch(search) {
    this.search = search;
    return this;
  }

  setHash(hash) {
    this.hash = hash;
    return this;
  }

  build() {
    return new Url(this.protocol, this.username, this.password,
      this.hostname, this.port, this.pathname, this.search,
      this.hash);
  }
}

const url = new UrlBuilder()
  .setProtocol('https')
  .setAuthentication('user', 'pass')
  .setHostname('example.com')
  .build();

Enter fullscreen mode Exit fullscreen mode

# Revealing constructor

Useful when we want to allow an object's internals to be manipulated only during its creation phase.

Use cases

  1. Creating objects that can be modified only at creation time
  2. Creating objects whose custom behavior can be defined only at creation time
  3. Creating objects that can be initialized only once at creation time

Syntax


//                  (1)               (2)       (3)
const object = new SomeClass(function executor(revealedMembers) { // manipulation code ...
})

Enter fullscreen mode Exit fullscreen mode

Immutable objects

Immutable refers to the property of an object by which its data or state becomes unmodifiable once it's been created.

  • Modifying an immutable object can only be done by creating a new copy and can make the code more maintainable and easier to reason about.
  • Efficient change detection with strict equality operator

Immutable version of the Node.js Buffer component


const MODIFIER_NAMES = ["swap", "write", "fill"];

export class ImmutableBuffer {
  constructor(size, executor) {
    const buffer = Buffer.alloc(size);
    const modifiers = {};
    for (const prop in buffer) {
      if (typeof buffer[prop] !== "function") {
        continue;
      }

      if (MODIFIER_NAMES.some(modifier => prop.startsWith(modifier))) {
        modifiers[prop] = buffer[prop].bind(buffer);
      } else {
        this[prop] = buffer[prop].bind(buffer);
      }
    }

    executor(modifiers);
  }
}

// USAGE

const hello = "Hello!";
const immutable = new ImmutableBuffer(hello.length, ({ write }) => {
  write(hello);
});

console.log(String.fromCharCode(immutable.readInt8(0)));

Enter fullscreen mode Exit fullscreen mode

A popular application of the Revealing Constructor pattern is in the JavaScript Promise class.

# Singleton

Reasons to use a single instance across all the components of an application:

  1. For sharing stateful information
  2. For optimizing resource usage
  3. To synchronize access to a resource

The module is cached using its full path as the lookup key, so it is only guaranteed to be a singleton within the current package.

❓ What happens when package-a and package-b have a dependency on the mydb package, but package-a depends on version 1.0.0 of the mydb package, while package-b depends on version 2.0.0 of the same package?


app/
`-- node_modules
    |-- package-a
    |  `-- node_modules
    |      `-- mydb
    `-- package-b
        `-- node_modules
            `-- mydb

Enter fullscreen mode Exit fullscreen mode

In such a case, a typical package manager such as npm or yarn would not "hoist" the dependency to the top node_modules directory, but it will instead install a private copy of each package in an attempt to fix the version incompatibility.

With the directory structure we just saw, both package-a and package-b have a dependency on the mydb package; in turn, the app package, which is our root package, depends on both package-a and package-b.


import { getDbInstance as getDbFromA } from 'package-a';
import { getDbInstance as getDbFromB } from 'package-b';
const isSame = getDbFromA() === getDbFromB();
console.log('Is the db instance in package-a the same ' +
  `as package-b? ${isSame ? 'YES' : 'NO'}`); // NO

Enter fullscreen mode Exit fullscreen mode

If instead, both package-a and package-b required two versions of the mydb package compatible with each other, for example, ^2.0.1 and ^2.0.7, then the package manager would install the mydb package into the top-level node_modules directory (a practice known as dependency hoisting), effectively sharing the same instance with package-a, package-b, and the root package.

# Wiring Modules using dependency injection

The Node.js module system and the Singleton pattern can serve as great tools for organizing and wiring together the components of an application. However, they might introduce a tighter coupling between components.

πŸ“– Dependency Injection (DI) is a very simple pattern in which the dependencies of a component are provided as input by an external entity, often referred to as the injector.

The injector has the goal of providing an instance that fulfills the dependency for the service.

Disadvantage of DI

Dependency Injection forces us to build the dependency graph of the entire application by hand, making sure that we do it in the right order. This can become unmanageable when the number of modules to wire becomes too high.

Alternative - Inversion of Control

πŸ“– Inversion of Control, allows us to shift the responsibility of wiring the modules of an application to a third-party entity. This entity can be a service locator (a simple component used to retrieve a dependency, for example, serviceLocator.get('db')) or a dependency injection container (a system that injects the dependencies into a component based on some metadata specified in the code itself or in a configuration file).

Top comments (0)