Design patterns and refactorings in JavaScript, notes
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()
withcreateXHR()
new Request(arg)
withcreateRequest(arg)
new Response(arg)
withcreateResponse(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