paint-brush
9 JavaScript Design Patterns You Will Loveby@pedrofullstack
4,236 reads
4,236 reads

9 JavaScript Design Patterns You Will Love

by Pedro HenriqueFebruary 6th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Design patterns are reusable solutions for common problems that occur during software development. They help us write code that is optimized and solves our problems. Because they are used by a lot of developers, you can be sure that they work. JavaScript's prototype inheritance can be explored to use several design patterns that help us develop better code.

People Mentioned

Mention Thumbnail

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - 9 JavaScript Design Patterns You Will Love
Pedro Henrique HackerNoon profile picture


Design patterns are reusable solutions for common problems that occur during software development. Every JavaScript programmer has encountered the same problems you have and the same solution has been used over and over again. Those solutions are the design patterns.


Every programming language has a lot of those solutions created by its community. This combined experience from several developers makes design patterns so useful. They help us write code that is optimized and solves our problems. Another great advantage is that being so common, different developers can easily understand each other code.


The greatest benefits from design patterns are:

  • Solutions that work: Because they are used by a lot of developers, you can be sure that they work. And not just that, but as the patterns have been used a lot of times, several optimizations were made.
  • Easily reusable: Design patterns are reusable by definition and even though they are very generic, they can be easily adapted to particular problems.
  • They are expressive: Design patterns can describe a complex solution in an elegant format.
  • Reduce the need for refactoring: When you write an application with design patterns in mind, it is easier to get to a clean code faster. This way we make fewer refactors. Especially in JavaScript, a language that allows for so many ways to write the same thing.
  • Makes your code smaller: As design patterns are usually optimized, they need less code to be implemented, and less code means fewer bugs.


With all that being said you should be already willing to start using design patterns on your projects. I will give a brief overview of the history of JavaScript, glance over some important features, and then we will dive deeper into the design patterns.


Some Important Features of JavaScript

We need to take a closer look at some aspects of the language that help us implement the design patterns. We can define JavaScript like this:


JavaScript is a light language, interpreted, object oriented with functions as first class citizens, that is mostly known as the language for web pages


That definition is to say that JavaScript has a small footprint on the system’s memory, it is easy to use, easy to learn, and has a syntax similar to other popular languages. Originally JavaScript was an interpreted language, but now it uses a JIT (just in time) compiler. Has support for procedural programming, object-oriented programming, and functional programming, which makes it very flexible (and also causes a lot of problems).


Now that we know what JavaScript is and its main characteristics let's see some features.

JavaScript supports first-class functions

That is something that may be harder to grasp if you are coming from languages like C or C++. To say that JavaScript treats functions as first-class citizens is to say that you can pass functions as parameters to other functions as a common argument. Almost as if they were objects.

JavaScript is based on prototypes

Like any other object-oriented language, JavaScript supports objects, and when we say objects we immediately think of classes and inheritance. This is where things get weird, originally JavaScript had no support for classes, and still uses an inheritance based on prototypes.


Prototype-based programming is a style that builds the reuse of features (the inheritance) by reusing already existing objects that are extended and not implemented. We will see this better on the design patterns examples. This characteristic is really important in a lot of patterns.

Event loops on JavaScript

If you have ever programmed on JavaScript, for sure you are familiar with the term callback. For those that are not, the callback is a function passed as a parameter that will be executed when an event is fired. They are usually used to listen for events like a mouse click or button press.


Every time an event that has a listener is fired, a message is sent to a queue that is processed synchronously, on a FIFO (first-in-first-out) manner. This is the event loop.


Each message in the queue has a function related to it. Every time a message leaves the queue, the function is completely executed before any other message is processed. It means that if a function contains other calls to other functions, they are all made before a new message from the queue is processed. This is called run-to-completion.

while (queue.waitForMessage()) {
    queue.processNextMessage();
}


The queue.processNextMessage() waits for new messages in a synchronous manner. Each one of the messages being processed has its own stack and is processed until the stack is empty. Once all the processes are finished, a new message is read from the queue, this goes on while there are messages on the queue.


You may have also heard that JavaScript is nonblocking. When an asynchronous operation is being processed, the program can go on processing other things, like new inputs, without blocking the main thread. This JavaScript property is very useful, in fact, that’s the reason a lot of people opt for the language outside the browser. This is a very interesting topic and deserves a post for himself.

What are Design Patterns?

Design patterns are commonly used solutions to common software development problems.

Proto-pattern

How a pattern is created? Say you recognized a recurring problem and you have a unique solution for this problem, one that has not been documented anywhere yet, not even on Stack Overflow. You use this solution every time you find this problem and believe that it is reusable and that all the development community would benefit from it.


Does your solution immediately become a pattern? Luckily, no. It is normal for someone with good development practices to mistake something that looks like a pattern, with something that actually is a pattern.


How can you know if what you have found is really a design pattern? By sharing it with other developers, programming is a team sport played by a large community. There is a phase through that every pattern must pass before being known as a design pattern, the proto-pattern.


A proto-pattern needs to be used and tested by several developers before becoming a real pattern. There is a huge work of documentation to be done for a pattern to be recognized and used by the community.

Anti-pattern

Just as a design pattern stands for good practice, an anti-pattern stands for bad practice.


A good example of an anti-pattern in JavaScript is to change the Object prototype. Practically all objects in JavaScript inherit from Object (remember, JavaScript uses a prototype-based inheritance) so imagine that you have changed this prototype. The changes to Object would be seen in every object that inherits from it — which would be almost all objects from JavaScript. A disaster waiting to happen!


Another example is to change objects you don’t know. A small modification on a method from an object used all around the application could cause a huge mess, the bigger the team the bigger the mess. You would have naming collision, incompatible implementations and a nightmare for maintenance. With luck, your tests would save you before anything happens.


In the same way that is important to know good practices, it is also a good idea to know the bad ones. This is a good way to realize the errors and avoid them before it is too late.

Design Patterns Categories

Design patterns can be categorized in several ways, here are the most popular:


  • Creational design patterns
  • Structural design patterns
  • Behavioral design patterns
  • Concurrency design patterns
  • Architectural design patterns


Creational Design Patterns

These patterns deal with object creation in a more optimized way than the basic object creation. When the regular object creation manner would cause more complexity or bring problems to the code, creational patterns can solve the problem.


Structural Design Patterns

These patterns work with relations between objects. They guarantee that if a system’s part change, nothing else has to change with it.


Behavioral Design Patterns

This kind of pattern acknowledges, implement and improve communication between objects of the system. They help to guarantee that unrelated parts of the application have synchronized information.


Concurrency Design Patterns

When you are dealing with multi-threading programming these are the patterns that you will want to use.


Architectural Design Patterns

Design patterns that are used on the system’s architecture, like MVC or MVVM.

In the next section, we will take a look at some of these patterns, with examples for better understanding.

Examples of My 9 Favorite Design Patterns

Each design pattern represents a solution for a given problem. There is no universal set of patterns that will solve every problem that you will encounter. We need to understand when a given pattern will be useful and how it will actually deliver value. Once we are familiar with design patterns and the cases on which they fit, we will be able to determine which pattern we can use and if it solves the problem at hand.


Remember, using the wrong pattern can lead to unwanted effects, unnecessary complexity, and performance loss.


These are all important things to be considered when we are thinking about applying a design pattern to our code. Let’s see some patterns that are more useful on JavaScript and every senior JavaScript developer should know.

1 Constructor Pattern

When we think of the classic implementation of object-oriented languages, a constructor is a special function that initializes the class’s values on default or with input from the caller.

Three common ways to create an object in JavaScript are:

// All of these forms can be used to create
// an object in JavaScript with the same result
let instance = {};
// or
let instance = Object.create(Object.prototype);
// or
let instance = new Object();


After our object is created we have four ways to add properties to it. They are:

// The . notation
instance.key = 'value';
 
// brackets notation
instance['key'] = 'value';
 
// defining a property with Object.defineProperty
Object.defineProperty(instance, 'key', {
    value: 'value',
    writable: true,
    enumerable: true,
    configurable: true
});
 
// defining multiple properties with Object.defineProperties
Object.defineProperties(instance, {
    'firstKey': {
        value: 'first key value',
        writable: true
    },
    'secondKey': {
        value: 'second key value',
        writable: false
    }
});


The most usual way for object creation is using {} and to add properties the dot notation or []. That is why I recommend that you use these methods, it will make a lot easier for other programmers to understand your code and even your future self.


We mentioned earlier that JavaScript does not support native classes, but it supports constructors through the keyword new prefixed to a function call. This way we can use a function as a constructor and initialize properties the same we would on a more traditional language.

// We define a constructor for objects of type Person
function Person (name, age, isDeveloper) {
    this.name = name;
    this.age = age;
    this.isDeveloper = isDeveloper || false;
 
    this.writesCode = () => {
        console.log(this.isDeveloper ? 'this person writes code' : 'this person does not writes code')
    }
}
 
// Create a person with: name = Ana, age = 32,
// isDeveloper = true and a method writesCode
let person1 = new Person('Ana', 32, true);
 
// Create a person with: name = Bob, age = 36,
// isDeveloper = false and a method writesCode
let person2 = new Person('Bob', 36);
 
// prints: this person writes code
person1.writesCode();
// prints: this person does not writes code
person2.writesCode();


We can still improve this code. The problem is that the method writesCode is redefined for every instance of Person. Because JavaScript is prototype-based we can avoid that by adding the method to the prototype.

// We define a constructor for Person
function Person(name, age, isDeveloper) {
    this.name = name;
    this.age = age;
    this.isDeveloper = isDeveloper || false;
}
 
// Then we extend the prototype, this way we make JavaScript
// point to this function when we call it on a Person
Person.protype.writesCode = function() {
        console.log(this.isDeveloper ? 'this person writes code' : 'this person does not writes code')
    }
}
 
// Create a person with: name = Ana, age = 32,
// isDeveloper = true and a method writesCode
let person1 = new Person('Ana', 32, true);
 
// Create a person with: name = Bob, age = 36,
// isDeveloper = false and a method writesCode
let person2 = new Person('Bob', 36);
 
// prints this person writes code
person1.writesCode();
// prints this person does not writes code
person2.writesCode();


Now, both instances of Person can access the same shared instance of writesCode().

It is as if on the first example we had a different method for each object of type Person, each Person points to their own function definition. In the second example, all Persons will point to the same function, the same piece of code.

2 Module Pattern

Despite being object-oriented, JavaScript does that in its own peculiar way. Because it does not have proper classes, it also can not limit access to a class’s components. On a language as Java or C++ you can define the access rights for a class’s members (private, protected, public, etc.), but being the clever little thing it is, JavaScript has a way to emulate this behavior.


Before we get into the details about module patterns, let’s take a better look at closures. A closure is a function with access to its parent’s scope, even after the parent has finished. They will help us emulate the behavior of access limiters. Let’s see an example:


// We use a immediately invoked function to create
// a private variable counter
var counterIncrementer = (() => {
    let counter = 0;
    return function() {
        return ++counter;
    };
})(); // these () in the end make this a immediately invoked function

// prints: 1
console.log(counterIncrementer());
// prints: 2
console.log(counterIncrementer());
// prints: 3
console.log(counterIncrementer());


As you can see, we tied the variable counter to a function that was called and closed, but we still can access it through the child function that increments the counter. Note that the inner function is returned from counterIncrementer(). We can not access the counter from outside the function, essentially, we created a private variable in vanilla JavaScript through scope manipulation.


Using closures we can even create objects with public and private parts. This is very helpful when we want to hide some parts of an object and only present a nice interface for the module’s user. The example:

// Using a closure we will expose an object
// as part of a public API that manages its
// private parts
let fruitsCollection = (() => {
    // private
    let objects = [];
 
    // public
    return {
        addObject: (object) => {
            objects.push(object);
        },
        removeObject: (object) => {
            let index = objects.indexOf(object);
            if (index >= 0) {
                objects.splice(index, 1);
            }
        },
        getObjects: () => JSON.parse(JSON.stringify(objects))
    };
})(); // notice the execution
 
fruitsCollection.addObject("apple");
fruitsCollection.addObject("orange");
fruitsCollection.addObject("banana");
 
// prints: ["apple", "orange", "banana"]
console.log(fruitsCollection.getObjects());
 
fruitsCollection.removeObject("apple");
 
// prints: ["orange", "banana"]
console.log(fruitsCollection.getObjects());


The greatest utility of this pattern is to make a clear separation between the public and private parts of an object.


Not everything is happiness, this pattern has some problems. When we want to change the visibility of a member, we must change that on every caller as well, because the access is different for public and private parts. Another problem is that methods added after the object is created can not access private methods (but we don't want to add new methods anyways).

3 Revealing Module Pattern

This is an evolution of the module pattern described above. The main difference is that we write all objects’ logic on the private scope and then expose what we want through an anonymous object. We can also change the private member'’ names when we map them to the public ones.

// We write the whole logic as private members
// and expose an anonymous object that maps the
// methods we want as their public counterparts
let fruitsCollection = (() => {
    // private
    let objects = [];
 
    const addObject = (object) => {
        object.push(object);
    }
 
    const removeObject = (object) => {
        let index = objects.indexOf(object);
        if (index >= 0) {
            objects.splice(index, 1);
        }
    }
 
    const getObjects = () => JSON.parse(JSON.stringify(objects))
 
    // public
    return {
        addName: addObject,
        removeName: removeObject,
        getNames: getObjects
    };
})();
 
fruitsCollection.addName("Bob");
fruitsCollection.addName("Alice");
fruitsCollection.addName("Frank");
 
// prints: ["Bob", "Alice", "Frank"]
console.log(namesCollection.getNames());
 
namesCollection.removeName("Alice");
 
// prints: ["Bob", "Frank"]
console.log(namesCollection.getNames());


The revealing module pattern is one of the ways we can implement a module pattern. The difference between the revealing module pattern and the other variants is mainly the way we reference the public modules. As a result, the revealing module pattern is a lot easier to be used and changed. It can still prove to be fragile in certain scenarios, when we will use the objects on an inheritance chain, for example. The most problematic situations are:


  1. If we have a private function referencing a public function, we can’t overwrite the public function. The private function will keep pointing to private implementation and it will lead to bugs.
  2. When we have a public member pointing to a private member and we try to overwrite the public member from outside the module, all other functions will still be referencing the private value, introducing bugs.

4 Singleton Pattern

When we want to allow a single instance of a class, we use the singleton pattern. For example, when we have a configurations object, we don’t want to create a new object each time it is called, it must always be the same, otherwise, we could have different settings each time.


let configurationSingleton = (() => {
    // private value of the singleton initialized only once
    let config;
 
    const initializeConfiguration = (values) => {
        this.randomNumber = Math.random();
        values = values || {};
        this.number = values.number || 5;
        this.size = values.size || 10;
    }
 
    // We export the centralized method to return
    // the singleton's value
    return {
        getConfig: (values) => {
            // initialize the singleton only once
            if (config === undefined) {
                config = new initializeConfiguration(values);
            }
 
            // and always return the same value
            return config;
        }
    };
})();
 
const configObject = configurationSingleton.getConfig({ "size": 8 });
// prints number: 5, size: 8, randomNumber: someRandomDecimalValue
console.log(configObject);
 
const configObject1 = configurationSingleton.getConfig({ "number": 8 });
// prints number: 5, size: 8, randomNumber: same randomDecimalValue // como no primeiro config
console.log(configObject1);


The random number generated is always the same, just as the values from config.

5 Observer Pattern

The observer pattern is very useful when we want to optimize the communication between separated parts of the system. It promotes the integration of the parts without making them too coupled.


you will find several different ways to implement this pattern, but the simpler case is when we have one emitter and lots of observers.


The emitter will execute all the operations to which the observers are subscribed. Those operations can be, subscribe to a topic, unsubscribe from a topic, and notify subscribers every time something is published to a topic.


One variant to this pattern is the publisher/subscriber pattern, which is the one we will see in this text.


On the observer pattern, the emitter keeps all the references to the observers and calls the methods directly on these objects. On the other hand, the publisher/subscriber pattern has channels that work as a communication layer between the publisher and the subscribers. The publisher fires an event and just executes the callback sent to this event.


Here is a small example of the publisher/subscriber pattern.

let publisherSubscriber = {};
 
// We pass an object to the container to manage subscriptions
((container) => {
    // the id represents a subscription to the topic
    let id = 0;
 
    // the objects will subscribe to a topic by
    // sending a callback to be executed when
    // the event is fired
    container.subscribe = (topic, f) => {
        if (!(topic in container)) {
            container[topic] = [];
        }
 
        container[topic].push({
            'id': ++id,
            'callback': f
        });
 
        return id;
    }
 
    // Every subscription has it's own id, we will
    // use it to remove the subscription
    container.unsubscribe = (topic, id) => {
        let subscribers = [];
        for (car subscriber of container[topic]) {
             if (subscriber.id !== id) {
                subscribers.push(subscriber);
            }
        }
        container[topic] = subscribers;
    }
    container.publish = (topic, data) => {
        for (var subscriber of container[topic]) {
            // when we execute a callback it is always
            // good to read the documentation to know which
            // arguments are passed by the object firing
            // the event
            subscriber.callback(data);
        }
    }
})(publisherSubscriber);
 
let subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", (data) => {
    console.log("mouseClicked, data: " + JSON.stringify(data));
});
let subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) {
    console.log("mouseHovered, data: " + JSON.stringify(data));
});
let subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) {
    console.log("second mouseClicked, data: " + JSON.stringify(data));
});
 
// When we publish an event, all callbacks should
// be called and you will see three logs
publisherSubscriber.publish("mouseClicked", {"data": "data1"});
publisherSubscriber.publish("mouseHovered", {"data": "data2"});
 
// We unsubscribe an event
publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3);
 
// now we have 2 logs
publisherSubscriber.publish("mouseClicked", {"data": "data1"});
publisherSubscriber.publish("mouseHovered", {"data": "data2"});


This design pattern is especially useful when we want to respond to a single event on different places. Imagine you need to make several requests to an API and depending on the responses you will make other calls. You would have to nest several callbacks and this might be difficult to manage. Using the publisher/subscriber pattern you solve this situation in a much simpler way.


The problem with this pattern are the tests. It might get hard to test the behavior of the publisher and listeners.

6 Mediator Pattern

A pattern that is used a lot in decoupled systems is the mediator. When we have different parts of a system that need to communicate in a coordinated manner, a mediator can be the best option.


The same way in real life, a mediator is an object that will be the focal point of communication between other objects.


At first sight, the mediator and publisher/subscriber patterns look very similar. And, in fact, they are both used to manage the communication between elements. The difference is that the publisher/subscriber throws the events to the wind and forgets about it, while the mediator will take care of each subscriber and be sure they deal with the message.


A good use case for the mediator pattern is the wizard. Say you have a long process of signing up on your system. Usually, large forms are broken into steps.


This is a way to make it easier to do maintenance on the code and at the same time, the user won’t be overwhelmed by a gigantic form. A mediator could manage the whole wizard, showing the use of different steps in tandem with the input from each step.


The obvious benefit from this pattern is the improvement in communication between the parts of the system. In the same way, it happens in a debate, the mediator assures that each person has time to speak and no one speaks out of order.


The mediator becomes a single point of failure, if it stops, everything stops. That is the main problem with this pattern.

7 Prototype Pattern

you must be getting tired of reading this, but JavaScript doesn’t support classes on its native form. Inheritance is made through prototypes.


We can create objects that will be used as prototypes for other objects to be created. The prototype is a blueprint for objects that the constructor makes. Almost like we differentiate classes and objects in Java.


Let’s see an example of this pattern.

let personPrototype = {
    sayHi: () => {
        console.log("Hi, I am " + this.name + ", and I have " + this.age);
    },
    sayBye: () => {
        console.log("Bye!");
    }
};
const Person = (name, age) => {
    let name = name || "John Doe";
    let age = age || 26;
 
    const constructorFunction = (name, age) => {
        this.name = name;
        this.age = age;
    };
 
    constructorFunction.prototype = personPrototype;
 
    let instance = new constructorFunction(name, age);
    return instance;
}
 
const person1 = Person();
const person2 = Person("Jane Doe", 38);
 
// prints Hi, I am John Doe, and I have 26
person1.sayHi();
// prints Hi, I am Jane Doe, and I have 38
person2.sayHi();


Note that inheritance by prototypes ends up bringing an improvement in performance as well, because both objects have a reference to the same method that is implemented on the prototype, instead of being implemented on each one of them.

8 Command Pattern

The command pattern is used when we want to encapsulate a call as an object. It is a way to keep separated the caller's context from the call.


Let’s say your JavaScript application has several calls to an API. Imagine now that the API changes. We don't want to change every place that interacts with the API.


That is where an abstraction layer separates the objects that call the API from the objects that determine when to call it comes in. This way we don’t need to do a big change on the application, because we just have to change the call to the API in a single place.


A problem that arises with this pattern is that it creates an additional abstraction layer, and it may impact the performance of an app. It is important to know how to balance performance and code legibility.

// The object that knows how to execute the command
const invoker = {
    add: (x, y) => {
        return x + y;
    },
    subtract: (x, y) => {
        return x - y;
    }
}
 
// the object to be used as abstraction layer when
// we execute commands; it represents a interface
// to the caller object
let manager = {
    execute: (name, args) => {
        if (name in invoker) {
            return invoker[name].apply(invoker, [].slice.call(arguments, 1));
        }
        return false;
    }
}
// prints 8
console.log(manager.execute("add", 3, 5));
// prints 2
console.log(manager.execute("subtract", 5, 3));

9 Facade Pattern

The facade design pattern is used when we want to create an abstraction layer between what is shown publicly and the internal implementation. It is used when we want to have a simpler interface.


This pattern is used, for example, on the DOM selectors of libraries as JQuery, Dojo, and D3. These frameworks have powerful selectors that allow us to write complex queries in a very simple way. Something like jQuery(".parent .child div.span") seems simple, but it hides a complex query logic underneath.


Here again, every time we create an abstraction layer above the code, we might end up having a loss of performance. Mostly this loss is irrelevant but is always good to be considered.

Conclusion

Design patterns are fundamental tools for every JavaScript developer. They collaborate a lot when it’s time do maintenance on the code by making everything simpler and easier to understand.


This post is getting really long, if you want to know more about design patterns I recommend the classic book Design Patterns: Elements of Reusable Object-Oriented Software from the Gang of four and also the more modern Learning JavaScript Design Patterns from Addy Osmani. Both books are really good and worth the reading (links are not affiliated).


This was a long post, thanks for reading it this far :)