January 25th, 2022
As developers it is our responsibility to write good code. This means writing code that is both efficient and readable. But sometimes we come across obstacles where we have to decide which code is better to go with when one sacrifices readability to perform better or vice versa.
This post will go over different chaining patterns in JavaScript and I hope that this will help you in any way possible when it comes to writing your chaining operations.
In JavaScript, method chaining is when methods are invoked from one object to another without creating intermediate variables. In otherwords it is a single statement of multiple method invocations which we instruct our program to perform.
Jquery is a good example of taking great advantage of its semantics because of its elegant ability to chain together its commands while encapsulating DOM APIs efficiently. It uses clear and concise syntax:
$(‘#main’).css(‘background’, ‘red’).height(200).css(‘text-align’,’center’).width(500);
This is an example of a single statement. In one line this does all of these in a single execution:
main
red
200px
center
500px
Another popular JavaScript library, expressjs utilizes method chaining to provide an easy and robust API for developers to use.
Here is an example taken from their error handling page:
var bodyParser = require('body-parser')
var methodOverride = require('method-override')
app.use(
bodyParser.urlencoded({
extended: true,
}),
)
app.use(bodyParser.json())
app.use(methodOverride())
app.use(logErrors)
app.use(clientErrorHandler)
app.use(errorHandler)
The logic is hidden within the implementation and returns this
to allow method chaining to continue:
app.use = function use(fn) {
var offset = 0
var path = '/'
// default path to '/'
// disambiguate app.use([fn])
if (typeof fn !== 'function') {
var arg = fn
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0]
}
// first arg is the path
if (typeof arg !== 'function') {
offset = 1
path = fn
}
}
var fns = flatten(slice.call(arguments, offset))
if (fns.length === 0) {
throw new TypeError('app.use() requires a middleware function')
}
// setup router
this.lazyrouter()
var router = this._router
fns.forEach(function (fn) {
// non-express app
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn)
}
debug('.use app under %s', path)
fn.mountpath = path
fn.parent = this
// restore .app property on req and res
router.use(path, function mounted_app(req, res, next) {
var orig = req.app
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err)
})
})
// mounted an app
fn.emit('mount', this)
}, this)
return this
}
code-block-writer makes it easy to write code with code with (yet again) the method chaining api:
const writer = new CodeBlockWriter()
writer.write('class MyClass extends OtherClass').block(() => {
writer.writeLine(`@MyDecorator(1, 2)`)
writer.write(`myMethod(myParam: any)`).block(() => {
writer.write('return this.post(').quote('myArgument').write(');')
})
})
The Builder Pattern by nature uses method chaining to achieve its goal. The builder by definition is a creational design pattern that lets you construct complex objects in a step by step fashion, effectively simplying the process.
Sounds pretty similar to how method chaining is described isn't it?
If we were to create a Chicken
class where its methods are to construct all of its body parts, we can use the builder to construct a full chicken:
const initBodyParts = () => ({
beak: null,
breast: null,
claw: null,
comb: null,
eye: { left: null, right: null },
eyeLobes: { left: null, right: null },
mainTail: null,
thigh: { left: null, right: null },
toe: { left: null, right: null },
wing: { left: null, right: null },
})
class ChickenEye {
constructor(options) {
this.options = options
}
close() {
//
}
}
class Chicken {
constructor(bodyParts) {
this.bodyParts = bodyParts
}
closeEyes() {
this.bodyParts.eye.left.close()
this.bodyParts.eye.right.close()
}
}
class ChickenBuilder {
constructor() {
this.bodyParts = initBodyParts()
}
addComb(options) {
this.bodyParts.comb = options
return this
}
addBeak(options) {
this.bodyParts.beak = options
return this
}
addBreast(options) {
this.bodyParts.breast = options
return this
}
addToe(options) {
this.bodyParts.toe[options.side] = options
return this
}
addClaw(options) {
this.bodyParts.claw = options
return this
}
addEye(options) {
this.bodyParts.eyes[options.side] = new ChickenEye(options)
return this
}
addEarLobes(options) {
this.bodyParts.earLobe[options.side] = options
return this
}
addMainTail(options) {
this.bodyParts.mainTail = options
return this
}
addWing(options) {
this.bodyParts.wing[options.side] = options
return this
}
addThigh(options) {
this.bodyParts.thigh[options.side] = options
return this
}
build() {
const chicken = new Chicken(this.bodyParts)
this.bodyParts = initBodyParts()
return chicken
}
}
const chickenBuilder = new ChickenBuilder()
const chicken = chickenBuilder
.addBeak({ shape: 'round' })
.addComb()
.addEye({ side: 'left' })
.addEye({ side: 'right' })
.addThigh({ side: 'left' })
.addThigh({ side: 'right' })
.addWing()
.addMainTail({ length: 2 })
.addToe({ side: 'left' })
.addToe({ side: 'right' })
.build()
If we didn't use a builder the code could become repetitive and it's not clear to consumers whether they return values or not which forces them to look for some documentation:
const chickenBuilder = new ChickenBuilder()
chickenBuilder.addBeak({ shape: 'round' })
chickenBuilder.addComb()
chickenBuilder.addEye({ side: 'left' })
chickenBuilder.addEye({ side: 'right' })
chickenBuilder.addThigh({ side: 'left' })
chickenBuilder.addThigh({ side: 'right' })
chickenBuilder.addWing()
chickenBuilder.addMainTail({ length: 2 })
chickenBuilder.addToe({ side: 'left' })
chickenBuilder.addToe({ side: 'right' })
const chicken = chickenBuilder.build()
If you've never used a builder before, take a look at this example below and see if you can spot the nice benefit it provides to us:
Without builder
let baseUrl = 'https://frogs.com'
let url = `${baseUrl}`
const pathname = `/api/v1`
const page = 2
const filter = 'date_added'
url = url + pathname + page + filter
With builder
let baseUrl = 'https://frogs.com'
let url = `${baseUrl}`
const builder = new UrlBuilder(baseUrl)
url = builder.page(2).filter('date_added').pathname('/api/v1').build()
By using the builder we saved multiple lines of code but we also hide the implementation details from the consumer. Hiding the implementation is whats important here.
This is the implemetation of the builder from the above snippet (yes I know it is long, but keep listening):
class UrlBuilder {
#filter = ''
#page = ''
constructor(baseUrl = '') {
this.baseUrl = baseUrl
}
pathname(value) {
this.pathname = value
return this
}
page(value) {
this.#page = value
return this
}
filter(value) {
this.#filter = value
return this
}
build() {
const append = (str = '', key = '', value) => {
if (!str.includes('?')) str += '?'
str += `&${key}=${value}`
return str
}
let url = `${this.baseUrl}/api/v1/${this.pathname}?`
this.#page && (url = append(url, 'page', this.#page))
this.#filter && (url = append(url, 'filter', this.#filter))
return url
}
}
The thing to take from this is that as developers our job is to create the efficient and readable code.
Yes our example without the builder was technically shorter, but is it reusable? Will we be able to work with people who can read the code as easily as we can (we wrote it).
The answer to both is no. When we share our code (or even when we try to reuse our code) we can easily pick up the builder and re-use it, as opposed to the earlier one. That is because we have to copy and paste the code if we were to try to re-use the earlier one.
When we copy and paste, we produce duplicate code. When code becomes duplicated, we have to make double the changes when we must writer updates to our api. And when we unnecessarily have to make double or triple the changes, our code becomes unmaintainable. The builder pattern is a simple but powerful pattern that solves all of those issues in one swoop!
The Chain of Responsibility (COR) is a pattern that allows some request to be sent, received, and handled by multiple objects. These objects (which are just functions) are not dependent on the implementation details of the previous nor the next request and can decide what to do when it runs its execution. They can also either abort the whole chain or decide to let the request continue on to the next object (or function) in the chain.
Here is an example of the pattern used in DOM:
<div id="root" onclick="onBtnContainerClick()">
<button>Say hello</button>
</div>
There's not really an "official" or correct way to implement this pattern as long as the functions can chain one after another in a controllable and predictable way.
ExpressJS also uses this pattern as well in their router to pass middleware handlers from one handler to the next:
app.get(
'/user/:id',
function (req, res, next) {
// if the user ID is 0, skip to the next route
if (req.params.id === '0') next('route')
// otherwise pass the control to the next middleware function in this stack
else next()
},
function (req, res, next) {
// send a regular response
res.send('regular')
},
)
But if you're wondering how exactly handlers get linked to eachother by next()
calls, it works exactly like how a linked list data structure works.
There's no right way to implement this pattern in practice but here is an example that links handlers together:
class Fighter {
constructor() {
this.hp = 100
this.next = null
}
fight(target) {
target.hp -= 25
this.next && this.next.fight(target)
}
}
class FistFighters {
constructor() {
this.fighters = []
}
addFighter(fighter) {
if (this.fighters.length) {
this.fighters[this.fighters.length - 1].next = fighter
}
this.fighters.push(fighter)
}
fight(target) {
this.fighters[0].fight(target)
}
}
const mob = new FistFighters()
const joe = new Fighter()
const michael = new Fighter()
const jacob = new Fighter()
const mojo = new Fighter()
mob.addFighter(joe)
mob.addFighter(michael)
mob.addFighter(jacob)
mob.addFighter(mojo)
const jim = new Fighter()
mob.fight(jim)
console.log(jim) // He died because his hp is down to 0 because the mob chained michael, jacob, mojo
Accessing nested objects is a dangerous operation because if our code doesn't handle when values are empty or simply whose prototype isn't inherited from the object prototype we can be thrown a TypeError
which can crash our program.
Here is what I mean:
const buckets = {
red: {
name: 'RedBucket',
items: [1, 2, 10],
},
}
const redBucketItems = buckets.red.items[1]
If we tried to access buckets.red.items[1]
when items
is accidentally set to a non-object like data type (like a null
value), we would get something unpleasant that looks like this:
Uncaught TypeError: Cannot read properties of null (reading '1')
JavaScript provides a way to make this easier called optional chaining which simplifies accessing values through objects that are connected which at some point might become null
or anything other than objects:
buckets.red.items[1]
// to:
buckets?.red?.items?.[1] // Result: undefined (but no crash!)
function fetchFrogs() {
return new Promise((resolve, reject) => {
return fetch('https://frogs.com/api/v1')
})
}
By chaining promises together you can write asynchronous code where each of your functions will be promised (literally) to receive their data in time:
function callMeWhenYouGetMyFrogs(frogs) {
window.alert(
`Here is a list of frogs fetched: ${JSON.stringify(frogs, null, 2)}`,
)
}
fetchFrogs()
.then((response) => response.json())
.then((data) => data.frogs)
.then((frogs) => callMeWhenYouGetMyFrogs(frogs))
.catch(console.error)
And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!
Tags
© jsmanifest 2023