DEV Community

Cover image for Design patterns in Node.js: Part 2
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Design patterns in Node.js: Part 2

Written by Fernando Doglio✏️

Welcome back to another installment of design patterns in Node.js, this is part two but if you want to review part one, where I covered the IIFE, Factory Method, Singleton, Observer and the Chain of Responsibility patterns, feel free to check it out, I’ll be here waiting.

But if you’re not interested or maybe already know about them, keep reading, since I’ll be covering four more patterns today.

I’ll try to provide as many real-life use cases as possible and keep the theoretical shenanigans to a minimum (there is always Wikipedia for that).

Let’s have some fun reviewing patterns, shall we?

Module pattern

The module pattern is definitely one of the most common ones because it seems to have been born out of the necessity for control over what to share and what to hide from your modules.

Let me explain. A very common practice in Node.js (and JavaScript in general), is to organize your code into modules (i.e set of functions that are related to each other, so you group them into a single file and export them out). By default, Node’s modules allow you to pick what to share and what to hide, so no problem there.

But if you’re either using plain old JavaScript or maybe have several modules inside the same file, this pattern helps you hide parts while, at the same time, letting you choose what to share.

LogRocket Free Trial Banner

What does it look like?

This module is heavily dependent on the IIFE pattern, so if you’re not sure how that one works, check out my previous article.

The way you create a module is by creating a IIFE, like this:

const myLogger = ( _ => {
    const FILE_PATH = "./logfile.log"
    const fs = require("fs")
    const os = require("os")

    function writeLog(txt) {
        fs.appendFile(FILE_PATH, txt + os.EOL, err => {
            if(err) console.error(err)
        })
    }

    function info(txt) {
        writeLog("[INFO]: " + txt)
    }

    function error(txt) {
        writeLog("[ERROR]: " + txt)
    }
    return {
        info, 
        error
    }
})()


myLogger.info("Hey there! This is an info message!")
myLogger.error("Damn, something happened!")
Enter fullscreen mode Exit fullscreen mode

Now, with the above code, you’re literally simulating a module that is exporting only the info and error functions (of course, that is if you were using Node.js).

The code sample is quite simple, but you still get the point, you can get a similar result by creating a class, yes, but you’re losing the ability to hide methods such as writeLog or even the constants I used here.

Use cases for the module pattern

This is a very straightforward pattern, so the code speaks for itself. That being said, I can cover some of the direct benefits of using this pattern in your code.

Cleaner namespace

By using the module pattern, you’re making sure global variables, constants or functions that your exported functions require, will not be available for all user code. And by user code, I mean any code that’ll be making use of your module.

This helps you keep things organized, avoid naming conflicts or even user code affecting the behavior of your functions by modifying any possible global variable you might have.

Disclaimer: I do not condone nor am I saying global variables are a good coding standard or something you should even be attempting to do, but considering you’re encapsulating them inside your module’s scope, they’re not global anymore. So make sure you think twice before using this pattern, but also consider the benefits provided by it!

Avoid import name collision

Let me explain this one. If you happen to be using several external libraries (especially when you’re working with plain JavaScript for your browser) they might be exporting their code into the same variable (name collision). So if you don’t use the module pattern like I’m going to show you, you might run into some unwanted behavior.

Have you ever used jQuery? Remember how once you include it into your code, besides the jQuery object, you also have available the $ variable at the global scope? Well, there were a few other libraries doing the same back in the day. So if you wanted your code to work with jQuery by using the $ anyways, you’d have to do something like this:

( $ => {
   var hiddenBox = $( "#banner-message" );
   $( "#button-container button" ).on( "click", function( event ) {
     hiddenBox.show();
   });
})(jQuery);
Enter fullscreen mode Exit fullscreen mode

That way, your module, is safe and has no risk of running into a naming collision if included in other codebases that already make use of the $ variable. And this last bit is the most important, if you’re developing code that will be used by others, you need to make sure it’ll be compatible, so using the module pattern allows you to clean up the namespace and avoid name collisions.

Adapter pattern

The adapter pattern is another very simple, yet powerful one. Essentially it helps you adapt one API (and by API here I mean the set of methods a particular object has) into another.

By that I mean the adapter is basically a wrapper around a particular class or object, which provides a different API and utilizes the object’s original one in the background.

What does it look like?

Assuming a logger class that looks like this:

const fs = require("fs")

class OldLogger { 

    constructor(fname) {
        this.file_name = fname
    }

    info(text) {
        fs.appendFile(this.file_name, `[INFO] ${text}`, err => {
            if(err) console.error(err)
        })
    }

    error(text) {
        fs.appendFile(this.file_name, `[ERROR] ${text}`, err => {
            if(err) console.error(err)
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

You already have your code using it, like this:

let myLogger = new OldLogger("./file.log")
myLogger.info("Log message!")
Enter fullscreen mode Exit fullscreen mode

If suddenly, the logger changes its API to be:

class NewLogger { 

    constructor(fname) {
        this.file_name = fname
    }

    writeLog(level, text) {
        fs.appendFile(this.file_name, `[${level}] ${text}`, err => {
            if(err) console.error(err)
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Then, your code will stop working, unless, of course, you create an adapter for your logger, like so:

class LoggerAdapter {

    constructor(fname) {
        super(fname)
    }

    info(txt) {
        this.writeLog("INFO", txt)
    }

    error(txt) {
        this.writeLog("ERROR", txt)
    }
}
Enter fullscreen mode Exit fullscreen mode

And with that, you created an adapter (or wrapper) for your new logger that no longer complies with the older API.

Use cases for the adapter pattern

This pattern is quite simple, yet the use cases I’ll mention are quite powerful in the sense that they work towards helping with isolating code modifications and mitigating possible problems.

On one side, you can use it to provide extra compatibility for an existing module, by providing an adapter for it.

Case in point, the package request-promise-native provides an adapter for the request package allowing you to use a promise-based API instead of the default one provided by request.

So with the promise adapter, you can do the following:

const request = require("request")
const rp = require("request-promise-native")

request //default API for request
  .get('http://www.google.com/', function(err, response, body) {
    console.log("[CALLBACK]", body.length, "bytes") 
  })


rp("http://www.google.com") //promise based API
  .then( resp => {
    console.log("[PROMISE]", resp.length, "bytes")
  })
Enter fullscreen mode Exit fullscreen mode

On the other hand, you can also use the adapter pattern to wrap a component you already know might change its API in the future and write code that works with your adapter’s API. This will help you avoid future problems if your component either changes APIs or has to be replaced altogether.

One example of this would be a storage component, you can write one that wraps around your MySQL driver, and provides generic storage methods. If in the future, you need to change your MySQL database for an AWS RDS, you can simply re-write the adapter, use that module instead of the old driver, and the rest of your code can remain unaffected.

Decorator pattern

The decorator pattern is definitely one of my top five favorite design patterns because it helps extend the functionality of an object in a very elegant way. This pattern is used to dynamically extend or even change the behavior of an object during run-time. The effect might seem a lot like class inheritance, but this pattern allows you to switch between behaviors during the same execution, which is something inheritance does not.

This is such an interesting and useful pattern that there is a formal proposal to incorporate it into the language. If you’d like to read about it, you can find it here.

What does this pattern look like?

Thanks to JavaScript’s flexible syntax and parsing rules, we can implement this pattern quite easily. Essentially all we have to do is create a decorator function that receives an object and returns the decorated version, with either of the new methods and properties or changed ones.

For example:

class IceCream { 
    constructor(flavor) {
        this.flavor = flavor
    }

    describe() {
        console.log("Normal ice cream,", this.flavor, " flavored")
    }
}

function decorateWith(object, decoration) {
    object.decoration = decoration
    let oldDescr = object.describe //saving the reference to the method so we can use it later
    object.describe = function() {
        oldDescr.apply(object)
        console.log("With extra", this.decoration)
    }
    return object
}

let oIce = new IceCream("vanilla") //A normal vanilla flavored ice cream...
oIce.describe() 

let vanillaWithNuts = decorateWith(oIce, "nuts") //... and now we add some nuts on top of it
vanillaWithNuts.describe()
Enter fullscreen mode Exit fullscreen mode

As you can see, the example is quite literally decorating an object (in this case, our vanilla ice cream). The decorator, in this case, is adding one attribute and overriding a method, notice how we’re still calling the original version of the method, thanks to the fact that we save the reference to it before doing the overwrite.

We could’ve also added extra methods to it just as easily.

Use cases for the decorator pattern

In practice, the whole point of this pattern is to encapsulate new behavior into different functions or extra classes that will decorate your original object. That would give you the ability to individually add extra ones with minimum effort or change existing ones without having to affect your related code everywhere.

With that being said, the following example tries to show exactly that with the idea of a pizza company’s back-end, trying to calculate the price of an individual pizza which can have a different price based on the toppings added to it:

class Pizza {
    constructor() {
        this.base_price = 10
    }
    calculatePrice() {
        return this.base_price
    }
}

function addTopping(pizza, topping, price) {

    let prevMethod = pizza.calculatePrice
    pizza.toppings = [...(pizza.toppings || []), topping]
    pizza.calculatePrice = function() {
        return price + prevMethod.apply(pizza)
    }
    return pizza
}

let oPizza = new Pizza()

oPizza = addTopping(
            addTopping(
                oPizza, "muzzarella", 10
            ), "anana", 100
        )

console.log("Toppings: ", oPizza.toppings.join(", "))
console.log("Total price: ", oPizza.calculatePrice())
Enter fullscreen mode Exit fullscreen mode

We’re doing something similar to the previous example here, but with a more realistic approach. Every call to addTopping would be made from the front-end into your back-end somehow, and because of the way we’re adding extra toppings, we’re chaining the calls to the calculatePrice all the way up to the original method which simply returns the original price of the pizza.

And thinking of an even more relevant example — text formatting. Here I’m formatting text in my bash console, but you could be implementing this for all your UI formatting, adding components that have small variations and other similar cases.

const chalk = require("chalk")

class Text {
    constructor(txt) {
        this.string = txt
    }
    toString() {
        return this.string
    }
}

function bold(text) {
    let oldToString = text.toString

    text.toString = function() {
        return chalk.bold(oldToString.apply(text))
    }
    return text
}

function underlined(text) {
    let oldToString = text.toString

    text.toString = function() {
        return chalk.underline(oldToString.apply(text))
    }
    return text
}

function color(text, color) {
    let oldToString = text.toString

    text.toString = function() {
        if(typeof chalk[color] == "function") {
            return chalk\[color\](oldToString.apply(text))
        }
    }
    return text
}

console.log(bold(color(new Text("This is Red and bold"), "red")).toString())
console.log(color(new Text("This is blue"), "blue").toString())
console.log(underlined(bold(color(new Text("This is blue, underlined and bold"), "blue"))).toString())
Enter fullscreen mode Exit fullscreen mode

Chalk, by the way, is a small little useful library to format text on the terminal. For this example, I created three different decorators that you can use just like the toppings by composing the end result from their individual calls.

The output from the above code being:

display of ui with chalk

Command pattern

Finally, the last pattern I’ll review today is my favorite pattern — the command pattern. This little fellow allows you to encapsulate complex behavior inside a single module (or class mind you) which can be used by an outsider with a very simple API.

The main benefit of this pattern is that by having the business logic split into individual command classes, all with the same API, you can do things like adding new ones or modifying existing code with minimum effect to the rest of your project.

What does it look like?

Implementing this pattern is quite simple, all you have to remember is to have a common API for your commands. Sadly, since JavaScript doesn’t have the concept of Interface , we can’t use that construct to help us here.

class BaseCommand {
    constructor(opts) {
        if(!opts) {
            throw new Error("Missing options object")
        }
    }
    run() {
        throw new Error("Method not implemented")
    }
}

class LogCommand extends BaseCommand{
    constructor(opts) {
        super(opts)
        this.msg = opts.msg,
        this.level = opts.level
    }
    run() {
        console.log("Log(", this.level, "): ", this.msg)
    }
}

class WelcomeCommand extends BaseCommand {
    constructor(opts) {
        super(opts)
        this.username = opts.usr
    }
    run() {
        console.log("Hello ", this.username, " welcome to the world!")
    }
}

let commands = [
    new WelcomeCommand({usr: "Fernando"}),
    new WelcomeCommand({usr: "reader"}),
    new LogCommand({
        msg: "This is a log message, careful now...",
        level: "info"
    }),
    new LogCommand({
        msg: "Something went terribly wrong! We're doomed!",
        level: "error"
    })
]

commands.forEach( c => {
    c.run()
})
Enter fullscreen mode Exit fullscreen mode

The example showcases the ability to create different commands which have a very basic run method, which is where you would put the complex business logic. Notice how I used inheritance to try and force the implementation of some of the methods required.

Use cases for the command pattern

This pattern is amazingly flexible and, if you play your cards right, can provide a great amount of scalability for your code.

I particularly like to use it in conjunction with the require-dir module because it can require every module in a folder, so you can keep a command-specific folder, naming each file after the command. This module will require them all in a single line of code and returns a single object with the keys being the filenames (i.e the commands names). This, in turn, allows you to keep adding commands without having to add any code, simply create the file and throw it into the folder, your code will require it and use it automatically.

The standard API will ensure you’re calling the right methods, so again, nothing to change there. Something like this would help you get there:

function executeCommand(commandId) {
  let commands = require-dir("./commands")
  if(commands[commandId]) {
    commands[commandId].run()  
  } else {
    throw new Error("Invalid command!")
  }
}
Enter fullscreen mode Exit fullscreen mode

With that simple function, you’re free to keep growing your library of commands without having to change anything! It’s the magic of a well-designed architecture!

In practice, this pattern is great for things like:

  • Taking care of the actions associated with a menu bar
  • Receiving commands from a client application, such as would be the case for a game, where the client application keeps sending command messages to the back-end server for it to process, run them and return the results
  • A chat server that receives events from different clients and needs to process them individually

The list can keep going since you can pretty much implement anything that is reactive to some form of input into a command-based approach. But the point here is the huge value added by implementing that logic (whatever it is for you). This way you gain amazing flexibility and ability to scale or re-factor with minimum effect on the rest of the code.

Conclusion

I hope this helped shed some light on these four new patterns, their implementations, and use cases. Understanding when to use them and, most importantly, why you should use them helps you gain their benefits and improve the quality of your code.

If you have any questions or comments about the code I showed, please leave a message down in the comments!

Otherwise, see you on the next one!


Editor's note: Seeing something wrong with this post? You can find the correct version here.

Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
Try it for free.


The post Design patterns in Node.js: Part 2 appeared first on LogRocket Blog.

Top comments (0)