Design patterns and refactorings in JavaScript, notes

Design patterns and refactorings in JavaScript

Even though JavaScript lacks strong types and interfaces, or has fake classes, that doesn't mean we shouldn't strive for better code.

There are always occasions for making our code cleaner, understandable, and maintainable, one step at a time.

Following, a collection of (work in progress) notes on software design patterns and refactorings applied to JavaScript. Feel free to peruse them.

// toc here

Creational patterns

Factory function

Factory function, also known as factory method is a pattern aiming at making collaborating part of the code loosely coupled.

It consists in replacing occurrences of new with a factory function which returns the expected instance.

The initial implementation

Consider the following example taken from chapter 9 of "The Little JavaScript Book", Working with asynchronous JavaScript:

"use strict";

window.fetch = fetch;

function Response(response) {
this.response = response.response;
this.ok = response.status >= 200 && response.status < 300;
this.statusText = response.statusText;
}

Response.prototype.json = function() {
return JSON.parse(this.response);
};

function Request(requestInit) {
this.method = requestInit.method || "GET";
this.body = requestInit.body;
this.headers = requestInit.headers;
}

function fetch(url, requestInit) {
return new Promise(function(resolve, reject) {
const request = new XMLHttpRequest();
const requestConfiguration = new Request(requestInit || {});
request.open(requestConfiguration.method, url);
request.onload = function() {
const response = new Response(this);
resolve(response);
};
request.onerror = function() {
reject("Network error!");
};
for (const header in requestConfiguration.headers) {
request.setRequestHeader(header, requestConfiguration.headers[header]);
}
requestConfiguration.body && request.send(requestConfiguration.body);
requestConfiguration.body || request.send();
});
}

It is a minimal, yet functional, Fetch polyfill, but it suffers from what I call new-itis. We call new no less than four times to construct instances of objects:

function fetch(url, requestInit) {
return new Promise(function(resolve, reject) {
const request = new XMLHttpRequest(); // bad
const requestConfiguration = new Request(requestInit || {}); // bad
request.open(requestConfiguration.method, url);
request.onload = function() {
const response = new Response(this); // bad
resolve(response);
};
//

I feel justified in this case because the polyfill is meant to have educational purposes, and design patterns really didn't fit in my book.

The use of new to create a Promise is not so bad, after all we want to wrap XMLHttpRequest in a Promise to offer a better ergonomic for our fellow developers. It can stay. How about the others?

The code is tied to specific implementations of: XMLHttpRequest, Request, and Response. What if we want to use different constructors, and be able to switch between different implementations in the future (or in testing)?

Refactoring to factory function

To refactor to factory functions (don't refactor without tests!) we move each constructor call to its own factory function. Then we use the factory function to build the desired instance:

function createXHR() {
return new XMLHttpRequest();
}

function createRequest(init) {
return new Request(init);
}

function createResponse(response) {
return new Response(response);
}

function fetch(url, requestInit) {
return new Promise(function(resolve, reject) {
const request = createXHR();
const requestConfiguration = createRequest(requestInit || {});
request.open(requestConfiguration.method, url);
request.onload = function() {
const response = createResponse(this);
resolve(response);
};

Here in the consumer code we replaced:

  • new XMLHttpRequest() with createXHR()
  • new Request(arg) with createRequest(arg)
  • new Response(arg) with createResponse(arg)

This refactor makes the code less coupled, easily testable, and easier to refactor in the future. Since we're no longer tied to a specific constructor we can swap the constructor in a single place, and the change propagates to all the consumers.

Work in progress

Bibliography

  • Design patterns, Elements of reusable object-oriented software - Gamma, Helm, Johnson, Vlissides
  • Refactoring, Improving the design of existing code - Second edition - Martin Fowler
  • Node.js design patterns - Third edition - Mario Casciaro, Luciano Mammino
Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!