Introduction
When you first start writing to code, any line of code that you write it looks like a small achievement. As you become more experienced and familiar with a programming language, you start to distinguish poorly and good written code, which at the same time makes you think of alternative ways on how you could “design” your code. As always, computer science has taken care of us and throughout these years has developed templates, commonly known as design patterns that provide a structure on how to solve common problems that appear in computer science. Design patterns are language-agnostic, meaning that whichever language you decide to use, it won’t make any difference, however, to demonstrate some common examples, I have chosen to use Javascript.
Programming Paradigms and Design Patterns
Although design patterns are language-agnostic, they are often bound to programming paradigms, with Object-Oriented and Functional Programming to be the most important one. Since JavaScript is a multi-purpose programming language, it provides support to both paradigms, with each one to be explained below:
Object-Oriented Programming (OOP): Javascript supports a prototype-based class model which allows to develop classes and inherit properties and methods from them.
Functional Programming (FP): Javascript supports higher-order functions, meaning that variables can be treated like any other data type. For example, a function can be assigned to a variable which is later passed as an argument to another function. In addition, ECMAScript2015 (ES6) have introduced a significant number of new features, including the map, filter and reduce functions that promote some design principles of functional programming
Modules
This is probably the most commonly used pattern in Javascript, especially after the introduction of ES modules in ECMAScript2015. The idea behind modules is scope isolation such as to avoid polluting the global scope with unnecessary code. Using modules, the code is packed to a separate scope and any variable assigned within it is kept internally, unless it is explicitly exported. To achieve that, JavaScript wraps a module to an Immediate Invoked Function Expression (IIFE), executes it, and exports any variables returned from it. This design pattern is applicable to OOP and FP, either by exporting a class or multiple functions from a single file.
// calculator_fp.js
const add = (a,b) => a + b
const substract = (a,b) => a - b
const multiply = (a,b) => a * b
const divide = (a,b) => a /b
export default {
add,
substract,
multiply,
divide
}
// calculator_oop.js
class Calculator {
add(a,b){
return a + b
}
substract(a,b){
return a - b
}
multiply(a,b){
return a * b
}
divide(a,b){
return a / b
}
}
export default Calculator
import calculatorFp from './calculator_fp'
import CalculatorOOP from './calculator_oop'
// FP
console.log(calculatorFp.add(1,1))
console.log(calculatorFp.substract(10,5))
console.log(calculatorFp.multiply(10,5))
console.log(calculatorFp.divide(10,5))
// OOP
const calculator = new CalculatorOOP()
console.log(calculatorFp.add(1,1))
console.log(calculatorFp.substract(10,5))
console.log(calculatorFp.multiply(10,5))
console.log(calculatorFp.divide(10,5))
Higher Order Functions
We often say that a language supports higher-order functions when functions are treated equally like any other data type. Javascript is one of them and allows developers to apply pure FP techniques within their code. Therefore, it is not so much a design pattern, rather than a feature available from the language that promotes the usage of other FP design patterns. Some examples are shown below:
const printHello = () => console.log("Hello World")
const returnHelloFn = () => {
return printHello
}
returnHelloFn()()
const passHelloFn = fn => fn()
passHelloFn(printHello)
Callbacks/Injection
If you have worked with asynchronous code in JavaScript, you have most likely come across callbacks. A callback is a piece of code that is passed to an outer function and you expect to call it in a later stage. Callbacks are commonly used in Javascript due to its asynchronous nature. For example, when an HTTP call is executed, Javascript won’t handle that event, rather than it will pass it to the event loop. The event loop is responsible to return the respect value running the code in the background, but, at the same time the Javascript engine will continue to execute its own code. When the result from the event loop is ready, it will handle it, and pass the result to the callback. Nonetheless, callbacks are not only used to handle asynchronous code, but also used to inject a piece of logic to certain parts of code, likewise one of the examples in higher order functions. As you may have noticed, callbacks require the language to support higher-order functions, and therefore that it’s mostly a FP technique.
const delay = (fn, duration = 1000) => {
return new Promise((resolve,reject) => {
if(duration < 0 ){
return reject("Please provide a positive duration")
}
const cb = duration => resolve(fn(duration))
setTimeout(cb, duration, duration)
})
}
const cb = data => console.log(`I have been called after ${data} milliseconds`)
delay(cb, 2000)
Closures
The use of closures is considered one of the best practises in programming since they minimise global variables and create variables within a private scope. This can happen when an internal function has access to the scope of an outer function. In Javascript, when a function is assigned, it creates a lexical environment that is composed by an Environment Record and a Pointer to an outer lexical environment. The former contains all the variables declared within a function, while the latter references the lexical environment of an outer function. With this in mind, we can access variables from an outer environment, which won’t be exposed to the global scope. In reality, all functions in Javascript are closures since they have access to their own scope, as well as an outer scope, the global scope. Similarly, to callbacks, closures depend on higher-order functions and if the language does not provide it, the closure design pattern is not possible.
const getCounter = () => {
let counter = 0
return () => ++counter
}
const counter1 = getCounter()
console.log(counter1())
console.log(counter1())
const counter2 = getCounter()
console.log(counter2())
Currying
Currying is a FP technique that translates the evaluation of a function that accepts multiple arguments, to a sequence of functions, each one accepting a single argument. You might be wondering what the meaning of this pattern is, and fortunately I have some good examples that use internally currying.
In testing and especially in unit testing, we aim to test the behaviour of a certain piece of logic and often to achieve that we need to mock all the dependencies of a unit/function. This is often achieved with dependency injection (DI), which simply suggests to pass those dependencies either as an object or arguments of a specific function. As a result, these dependencies are moved to the function signature, however, it's cumbersome and repetitive to pass them whenever we actually want to call the function. In that case we can use currying to break down the single invocation of a function, to multiple smaller ones. For example, we can break a function to accept all its dependencies in the first call, and in the second call to accept the actual arguments that the function needs to execute its logic. Thus, the first call will be more like an initialization step, whereas the second one will focus on the actual code. In addition, since currying gets control of all dependencies of a function and passes them as arguments, writing pure functions becomes extremely easy.
const curried = fn => function handleCurrying(...args){
if(args.length < fn.length){
return (...newArgs) => handleCurrying(...args, ...newArgs)
} else {
return fn(...args)
}
}
const addition = (a,b,c) => a + b + c
const additionCurried = curried(addition)
console.log(additionCurried(1))
console.log(additionCurried(1)(2))
console.log(additionCurried(1)(2)(3))
const handleHTTPRequest = httpClient => async options => httpClient(options)
const customClient = options => Promise.resolve({...options, status: 200});
(async () => {
const fetch = handleHTTPRequest(customClient)
// note that fetch is used in both cases to fetch data, without repeating ourselves passing the customClient
const res = await Promise.all([
fetch({method: 'GET', headers: { 'Accept': 'application/json'}}),
fetch({method: 'POST', headers: {'Content-Type': 'application/json'}, body: { username: "test", password: '12345678'}})
])
console.log(res)
})();
Recursion
A recursion is a design pattern in functional programming with a function to call itself until it meets a condition. When a condition is met, it is called the base of recursion and the number of nested calls recursion depth. Since recursion repeatedly invokes a function until it meets a condition – this means the creation of an execution context and a lexical environment which is pushed to the call stack – browsers handle that by setting a limit, which if its exceeded, a maximum call stack error is thrown. The most important thing to understand in recursion is finding the recursion base, and as soon as you have that, one more powerful skill will be added to your toolset. Saying that, in pure FP languages, recursion (recursive logic) is a replacement of loops (iterative logic), and we demonstrate that in a simple example below:
const find = (stack = [], index) => {
if(index > stack.length - 1 || index < 0 ){
return null
}
let counter = 0
const handleFind = (stack, index) => {
const value = stack.shift()
return index === counter ? value : ++counter && handleFind(stack, index)
}
// since arrays are pass by reference in js, we recreate it so we avoid any side-effects
return handleFind(stack.slice(0),index)
}
const arr = [10,5,101,14,2]
console.log(find(arr, 2)) // 101
console.log(find(arr, -1)) // null
console.log(find(arr, 5)) // null
console.log(find(arr, 4)) // 2
Memoization
Nowadays, applications tend to repeat tasks that already have been executed in the past. Memomization solves that problem by introducing a storage unit(cache) which is used internally and stores any calculation made by the function. Usually, the arguments passed to a function are transformed to a unique key that is used to access a value from the cache. If the key actually exists, the returned value will be what is inside the cache. If not, the function is normally executed, the result is stored to the cache, so it won’t be repeated the next time, and returned. A good example of memorization is the Fibonacci series because it performs recursive calls to already calculated values, so let’s see an example of it.
const fibMemo = () => {
const cache = new Map()
return function fib(n){
if(n < 0){
return null
}
if (cache.has(n)){
return cache.get(n)
} else {
const result = n < 2 ? n : fib(n-1) + fib(n-2)
cache.set(n, result)
return result
}
}
}
const fib = n => {
if(n < 0){
return null
}
return n < 2 ? n : fib(n-1) + fib(n-2)
}
const benchmark = fn => (...args) => {
const start = Date.now()
const value = fn(...args)
const end = Date.now()
console.log(`value: ${value}\ttime: ${end-start}ms`)
}
const fibMemoBench = benchmark(fibMemo())
const fibPlainBench = benchmark(fib)
fibMemoBench(40) // value: 102334155 time: 0ms
fibPlainBench(40) // value: 102334155 time: 1312ms
Declarative Functions
While imperative programming deals with the implementation details of a certain task, map, filter and reduce abstract those details away and provide an API that allows easy iteration over a data structure and promotes declarative programming. Thus, declarative programming focused on how to get the right result rather than implementing the right code logic. For example, when you need to calculate the sum of an array structure, reduce already provides a functionality to iterate and manipulate the values at each step, rather than implementing your own for-loop.
const arr = [2,4,9]
// reduce
const sum = (...args) => args.reduce((acc,v) => acc + v,0)
console.log(sum(...arr)) // 15
// map
const sqrt = n => n ** 0.5
console.log(arr.map(sqrt))
// filter
const condition = n => n > 4
console.log(arr.filter(condition))
Decorator
A decorator is an important concept in functional and object-oriented programming that enhances the existing functionality of a method/function. Recently I was working in a React-based application and I had a function-based component – let’s call it X – that was used in a lot of places within the app. This means that a lot of other components depended from X, but that dependency was a result for keeping our app clean. Before I started refactoring the app, all these dependent components were doing some pre-processing of their data, so they passed the right values to X, however, this led to increased code. Part of the refactoring was to introduce a decorator that accepts any value from the dependent components of X, processes it, and passes the right result down to X. By wrapping X within the decorator, it increased its existing functionality, but at the same time didn’t pollute the component with redundant code that wasn’t directly related to it.
In order to demonstrate the concept of a decorator, in the code below we wrap a function that calculates a fibonacci sequence around a cache so that we improve its performance.
const fib = n => {
if(n < 0){
return null
}
return n < 2 ? n : fib(n-1) + fib(n-2)
}
const decorator = fn => {
const cache = new Map()
return (...args) => {
const key = args.join('')
if(cache.has(key)){
return cache.get(key)
} else {
const result = fn(...args)
cache.set(key, result)
return result
}
}
}
const benchmark = fn => (...args) => {
const start = Date.now()
const value = fn(...args)
const stop = Date.now()
console.log(`value: ${value}\ttime: ${stop-start}ms`)
}
const newFib = benchmark(decorator(fib))
newFib(40) // value: 102334155 time: 1297ms
newFib(40) // value: 102334155 time: 0ms
Singleton
Singleton is the most famous design pattern for OOP and suggests the instantiation of a class that will hold the global state of an application. To access the state of the class you can use getter and setters, however for large scale applications this pattern is often discouraged. We show this pattern in conjunction with the cascade/chaining pattern.
Cascade/Chaining
Cascade is a simple but powerful design pattern in OOP that allows consecutive manipulations of the internal state of an object. We can achieve that in Javascript by returning the instance of a class in any call of its methods. Let’s demonstrate that with an example:
class Calculator {
constructor(value){
this._value = value
}
set value(value){
this._value = value
}
get value(){
return this._value
}
add(v){
this._value = this._value + v
return this
}
substract(v){
this._value = this._value - v
return this
}
multiply(v){
this._value = this._value * v
return this
}
divide(v){
this._value = this._value / v
return this
}
}
const calculator = new Calculator(0)
const value = calculator.add(10)
.substract(2)
.multiply(3)
.divide(4)
.value
console.log(value) // 6
Summary
Although we have mentioned a bunch of patterns, we have only scratched the surface of what is available in computer science. What we have covered its only my personal preference, however, take the time to explore and find those design patterns that fit well with the programming language of your own preference. Design patterns provide alternative ways on how to structure your code, and as soon as you understand them, that it is a step for being a better programmer. I have created a repository with all the examples mentioned in the article, so if you are interested, have a look in the link.