Javascript Patterns #1: Creational

Javascript Patterns #1: Creational

Featured on Hashnode

Have you encountered similar problems in different projects? And You have been quite a tricky resolve it? If your response is YES, let me tell you that maybe someone has already solved your problem using design patterns.

The design patterns are closely related to object-oriented languages, but we can apply them in Javascript to take advantage of its flexibility.

In this post, let's look at applying the design patterns through practical examples using Javascript and NodeJS as runtime.

What is a Design Pattern?

In a nutshell, the design patterns are reusable solutions that can apply to almost all design problems; the concepts behind these patterns have been tested for a long time for solving different situations.

I believe that by using them, we benefit from the following:

  • Reach a ubiquitous language to maintain fluid communication with other team's roles such as developers or architect solutions.

  • A set of ready-to-use solutions that we can adapt to our necessity allowing reduce resolution time of any problems.

Today, we will review the creational patterns, focusing on the Factory and the Builder pattern.

Creational Patterns

As its title suggests, these patterns try to solve problems about creating objects. Let's review some of the most used.

Factory Pattern

To catch the concepts behind this pattern, applying it through an example is essential to fully understanding; therefore, let's develop a Color Console that it's a command-line utility that allows us to enter a message and log it with a particular color in the console.

factory-pattern.gif

First, We need to create a base class named ColorConsole, that defines a "log" method that receives a message.

export class ColorConsole {
    log(message) {}
}

Later, we create two child classes that implement the "log()" method from the parent class: YellowConsole and RedConsole; both use the popular library "chalk" to style the messages displayed in the console.

In the case of the RedConsole class, use the "red()" method from chalk to style the message.

import { ColorConsole } from "./colorConsole.js";
import chalk from "chalk";

export class RedConsole extends ColorConsole {
    log(message) {
        console.log(chalk.red(message));
    }
}

On the other hand, the YellowConsole class uses chalk's "yellow()" method to style the message.

import { ColorConsole } from "./colorConsole.js";
import chalk from "chalk";

export class YellowConsole extends ColorConsole {
    log(message) {
        console.log(chalk.yellow(message))
    }
}

We can reach the same result without library chalk only using the Console Module of NodeJS; for more information, review the following link console-color.

Finally, we implement the command-line logic; I leverage the "yargs" library to parse the arguments entered in the console.

#!/usr/bin/env node

// dependencies needed by yargs
import process from 'process';
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
const argv = yargs(hideBin(process.argv)).argv;


// dependencies required to display the message
import {RedConsole} from "./redConsole.js";
import {YellowConsole} from "./yellowConsole.js";


const { message, style } = argv;

// object creation logic
if(style === "red"){
    const redConsole = new RedConsole();
    redConsole.log(message)
}else if(style === "yellow"){
    const yellowConsole = new YellowConsole();
    yellowConsole.log(message)
}else{
    console.log("Style not setting")
}

The more relevant in the above portion code is the creation object section, which returns a type console depending on the style entered by the client. But what happens if we extend the available types by adding a new color, for instance, blue; then we need to make the following:

  • Create a new child class named BlueConsole that implements the "log()" method from the parent.

  • Add the logic of creating the BlueConsole in the CLI, as follow:

if(style === "red"){
    const redConsole = new RedConsole();
    redConsole.log(message)
}else if(style === "yellow"){
    const yellowConsole = new YellowConsole();
    yellowConsole.log(message)
}else if(style === "blue"){ //logic added
    const yellowConsole = new YellowConsole();
    yellowConsole.log(message)
}else{
    console.log("Style not setting")
}

The principal problem with this solution is dealing with the coupling into implementing the command-line utility(consumer). If we want to support new console types, we must modify the code associated with the CLI.

At this point, we have identified the design problems whereby we can use the factory pattern to solve them. First, we have separated the logic related to the creation console in a new object named Console Factory.

import { RedConsole } from "./redConsole.js";
import { YellowConsole } from "./yellowConsole.js";

const buildConsole = (type) => {
    if (type === "red") {
        return new RedConsole();
    } else if (type === "yellow") {
        return new YellowConsole();
    }

    throw new Error("Color type doesn't defined");
};

export default buildConsole;

As I mentioned, the factory now handles the details of object creation(encapsulations), allowing the CLI to forget about that responsibility.

#!/usr/bin/env node

// dependencies needed by yargs
import process from 'process';
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
const argv = yargs(hideBin(process.argv)).argv;


// factory imported
import consoleFactory from "./consoleFactory.js";

const { message, style } = argv;

// logic to log message
const colorConsole = consoleFactory(style);
colorConsole.log(message);

By encapsulating the creation of consoles in a new module, also we achieved the following:

  • Support new entry points, such as a desktop application that reuse the module created(factory).

  • We have only one place to make modifications when we need to support new types of consoles.

FactoryDesignPattern.png

Builder Pattern

We have had to create objects with many parameters at some point in our careers. For example, imagine that we have a class named User as the following.


class User{
    constructor(firstName,
                lastName,
                address,
                phone,
                email
                //...paramN
    ){}
}

Invoking such a constructor would create some hard-to-read. Take the following code, for example:

//Object created with all the parameters
const person1 = new User("FirstName", "LastName", "XXXXXX", "My house", "59XXXXXXX", "hello@mail.com");
//Obbject created with some parameters
const person2 = new User("FirstName", "LastName", "XXXXXX");

We can improve the creation of objects using The Builder Pattern. The Builder pattern allows tackling the creation of complex objects in one way easy; offering additional benefits such as:

  • Break down a complex constructor into more readable and maintainable steps.

  • Build objects step by step, allowing you to manipulate parameters to apply validation, casting, or normalization before passing them to the class constructor.

For a better understanding, let's develop a Url Builderfor a better understanding, where we will appreciate the benefits of applying it. The example is based on the native module URL included in NodeJS nodejs.org/api/url.html#url.

First, we define a class named Url, where we'll declare all parameters needed that allow constructing a URL.

export default class Url {
    constructor(protocol, hostname, host, port, path, parameters) {
        this.protocol = protocol;
        this.hostname = hostname;
        this.host = host;
        this.port = port;
        this.path = path;
        this.parameters = parameters;
        this.validate();
    }

    validate() {
        if (!this.protocol || (!this.hostname && !this.host && !this.port)) {
            throw new Error(
                "Must specify at least a protocol, hostname or host and port"
            );
        }
    }

    toString() {
        const queryParameters = this.parameters ? `?${this.parameters}` : "";
        const ref = this.hostname ?? `${this.host}:${this.port}`;
        return `${this.protocol}://${ref}${this.path}${queryParameters}`;
    }
}

A URL is a string structured for some components, such as a hostname, protocol, pathname, port, etc.; the class' constructor is inevitably big.

return new Url('https','example.com', null, null, null,
  null)

This scenery is perfect for applying the Builder Pattern. We must create a Builder class with a setter method for each parameter required to instantiate the Url class; it also contains the "build()" method to retrieve a new Url instance created using all the parameters that have been set in the builder.

import Url from "./url.js";

export default class Builder {

    setProtocol(protocol) {
        this.protocol = protocol;
        return this;
    }

    setPath(path) {
        this.path = path;
        return this;
    }

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

    setHost(host) {
        this.host = host;
        return this;
    }

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

    setQuery(query) {
        this.query = query;
        return this;
    }

    build() {
        return new Url(this.protocol, this.hostname, this.host, 
                    this.port,this.path, this.query);
    }
}

Finally, we can do a file named index.js to test the functionality.

import Builder from "./urlBuilder.js";

const httpsUrl = new Builder()
  .setProtocol("https")
  .setHostname("hashnode.com")
  .setPath("/explore")
  .build();
console.log(httpsUrl); // https://hashnode.com/explore

const ftpUrl = new Builder()
  .setProtocol("ftp")
  .setHostname("fileservername")
  .setPath("/path")
  .build();
console.log(ftpUrl); // ftp://fileservername/path

Summary

  • To apply any design pattern is very important to discover the problem.

  • Although javascript is not a completely object-oriented language, it is possible to adjust the design patterns due to the flexibility of the language.

  • The main advantage of using the factory pattern is decoupling the creation of an object from one particular implementation.

  • The builder pattern helps us when we need to build a complex object step by step.

Reference