JavaScript Design Patterns

by Nicklas Envall

This article will introduce you to design patterns and how to implement three common design patterns with JavaScript. The goal is to be concise, but by doing so, we also miss to cover things. For a more in-depth look, there are plenty of books and video tutorials out there. See this as an introduction or a concise refreshment for your memory.

What is a design pattern?

A design pattern is a solution to a problem in a context. The documentation of a design pattern has a special structure and contains different components such as name, description, context outline, etc. Which sounds pretty good. But why should you learn design patterns? There are many reasons, two of them being:

  • Communication: a prominent benefit of patterns is a shared vocabulary, amongst your fellow developers. Instead of doing a time-consuming and confusing explanation, you say which pattern you used or want to use.

  • Proven solutions: before a new design pattern is accepted, it needs to be rigorously tested and used by multiple developers. Which means a pattern is a proven solution. So why would you try to reinvent the wheel and waste your time?

But there's much more to the world of design patterns. For example, the opposite of a design pattern is an anti-pattern, which is a pattern that shows you what not to do. In other words, how to go from a problem to a bad solution. The reason for documenting anti-patterns is to make sure that we're aware of the consequences. Anti-patterns also contain a description of how to go from a bad implementation to a good one. An example of an anti-pattern in JavaScript is, defining many variables globally.

Categories of Patterns

There are categories for Design patterns. The most common categories are Creational, Behavioral, and Structural. Let’s have a closer look at what they mean:

  • Creational Patterns: are concerned about instantiating objects while also decoupling the client from the objects being instantiated.
  • Behavioral Patterns: deal with how objects interact and assign responsibility.
  • Structural Patterns: is concerned with how to compose objects into larger structures.

The following image illustrates which category the design patterns have. Be aware that there are more patterns out there.

design patterns categories

Implementing Design Patterns in JavaScript

There is a sea of patterns out there, and I’ll cover three. If you do not know or want to refresh your memory on how to work with objects in JavaScript before we continue, then you can read our article about objects and prototypes in JavaScript. The following patterns will be covered:

  • The Module Pattern
  • The Observer Pattern
  • The Adapter Pattern

The Module Pattern

Modular programming allows us to separate functionalities of our program into modules. With modules, we can organize our code to achieve a good structure for our programs. Our modules may be dependent on other modules, and we decide what our modules should and shouldn’t make visible (public and private).

“I don’t think it’s an exaggeration to suggest that the single most important code organization pattern in all of JavaScript is, and always has been, the module.”

- Kyle Simpson 2015

There was no built-in module system before ES6. But, that did not stop developers from creating module systems. By using closure we can maintain a private internal state which gives us some privacy. A common approach was to invoke an anonymous function immediately (known as IIFE) that returned a public API:

var musicPlayer = (function MusicPlayer() {
  
    // private
    var secretSong = 'Jingle Bells';
  
    function playSecretSong() {
       console.log('playing the secret song: ' + secretSong);
    }
  
    // public api
    return { playSecretSong };
})();
 
musicPlayer.playSecretSong(); // playing the secret song: Jingle Bells
musicPlayer.secretSong; // undefined

The example above displays an old approach. This article’s focus is on patterns and not history, but it can be confusing with all the different ways to create a module. At the time of writing this, we have ES6 modules, but prior to ES6, we depended on module systems such as CommonJS and AMD (asynchronous module definition). You might have heard of CommonJS because NodeJS currently uses it.

Anyway, now with ES6, we have built-in support for modules. With ES6 modules, we do not need to use functions as we did above. Instead, an ES6 module is a file. Now we'll implement what we did above, but with ES6 modules. We'll need to use export and import, what we export is made public and everything else is private:

// src/musicPlayer.js
const secretSong  = 'Jingle Bells';
 
export const playSecretSong = () => console.log('playing the secret song: ' + secretSong);
 
export default { playSecretSong };

Then we use import in another file to import our module or parts of our module.

// src/index.js
import musicPlayer, { playSecretSong } from './musicPlayer';
 
playSecretSong();  // playing the secret song: Jingle Bells
musicPlayer.playSecretSong(); // playing the secret song: Jingle Bells
musicPlayer.secretSong; // undefined

There's much more to learn about modules in JavaScript, see this as an introduction to the module pattern.

The Observer Pattern

The Observer Pattern defines a one-to-many dependency between objects. It ought to be implemented by having at least one subject and one or more observers. The subject's job is to store all the observers and notify them when the subject's state changes.

With the Observer Pattern, our objects can interact but are loosely coupled. You’ve probably used something similar to the Observer Pattern without being aware of it, just think of, addEventListener. Look at the following code:

const observer1 = () => console.log('I changed!');
const observer2 = () => console.log('I also changed!');
document.addEventListener('click', observer1);
document.addEventListener('click', observer2);

We are listening to one subject (the document), and once it changes it notifies all the observers it keeps track of. You could also argue that this pattern is the Publish/Subscribe pattern, more on this later. For now, just be aware that this pattern is essentially the Observer Pattern.

Let’s create a subject (also known as observable):

const createSubject = () => {
   let observers = new Set();
 
   const addObserver = (observer) => { observers.add(observer); }
 
   const removeObserver = (observer) => observers.delete(observer);
 
   const notifyObservers = (message) => {
      observers.forEach((observer) => observer.notify(message));
   }
 
   return { addObserver, removeObserver, notifyObservers };
}

Our subject has a collection of observers. In this example, we notify our observers “manually” with notifyObservers(msg) , also note that each observer, in this case, requires a notify function. Now we need our observers, so let’s create them:

const createObserver = (onNotify) => ({
   notify(message) {
       onNotify(message);
   }
});

Examine the code above on your own. Now we’ll use it all together:

const subject = createSubject();
const observer1 = createObserver((message) => console.log('I got the message: ' + message));
const observer2 = createObserver((message) => console.log('Me too its:  ' + message));

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notifyObservers('hey all');

// OUTPUT:
// I got the message: hey all
// Me too its: hey all

Now we’ve implemented The Observer Pattern. Let’s compare our solution to addEventListener:

document.addEventListener('click', someObserver);
subject.addObserver(someObserver);

As you see, with subject all the observers will get notified when we use notifyObservers. But with addEventListener, we subscribe to a certain event (click). We can achieve something similar with the Publish/Subscribe pattern, let’s implement our own faked document:

const createDocument = () => {
   const subscribers = {};
 
   const publish = (event, ...args) => {
       if (subscribers[event]) {
           subscribers[event].forEach(callback => callback(...args));
           return true;
       }
       return false;
   }

   const subscribe = (event, callback) => {
       if (subscribers[event]) {
           subscribers[event] = [...subscribers[event], callback];
       } else {
           subscribers[event] = [callback];
       }
   }

   return { publish, subscribe };
}
const doc = createDocument();
 
doc.subscribe('lunch', (msg) => console.log(msg));
doc.subscribe('lunch', (msg, meal) => console.log(msg, meal));
doc.subscribe('dinner', (meal) => console.log(meal));
 
 // OUTPUT:
doc.publish('lunch', 'its lunch now!', 'pizza');
// its lunch now!
// its lunch now! pizza
doc.publish('dinner', 'potatoes');
// potatoes

The Adapter Pattern

The Adapter pattern converts the interface of a class into another interface that the clients expect. In JavaScript, the Adapter pattern ensures our objects work the same even though they do not have the same properties. It's a little different because we do not have "real interfaces" or classes, with JavaScript, we only have to try to invoke a method on the object we're working with.

Furthermore, the Adapter pattern is similar to the Facade pattern. But, the Adapter and Facade pattern is not the same pattern. A facade intends to simplify, while an adapter converts the interface to something.

Let’s create a scenario where we could use the Adapter pattern:

const createCat = () => ({ meow: () => console.log('meeeaoow') });
const createDog = () => ({ bark: () => console.log('woof woof') });

const animals = [createCat(), createDog()];
 
animals.forEach(animal => animal.bark());

This would result in the error, TypeError: animal.bark is not a function. So now we’ll create an adapter that’ll convert our cat so it suits the client’s needs:

const createCat = () => ({ meow: () => console.log('meeeaoow') });
const createDog = () => ({ bark: () => console.log('woof woof') });
const createCatAdapter = cat => ({ bark: () => cat.meow() });
 
const animals = [createCatAdapter(createCat()), createDog()];
 
animals.forEach(animal => animal.bark());

You could argue that we should’ve coded differently from the start, or that the semantics does not make sense, which are valid points. But this is only to show you the main idea of the pattern, the interface is not what our client expects, so we adapted it.

More patterns await...

There are many more patterns and things to know about design patterns. I recommend looking up Head First Design Patterns, it's in Java, but it's a great introduction to patterns. I also recommend the Gang of Four (Design Patterns: Elements of Reusable Object-Oriented Software) book, even though I haven't read it yet, because it's the book about design patterns.

But a word of caution before you start using patterns. Be aware that patterns can be powerful, but using them only for the sake of using them will hurt more than it helps. If you can find a good simple solution without patterns, then use that one! But if you find yourself in a particular context with a specific problem, well, then that just might be an excellent opportunity to use a pattern.