Conditional statements are one of the first concepts you learn. They are convenient for controlling the flow of your code. And every program, from small scripts to large enterprise applications, uses them extensively.
Until that crucial moment, the codebase grows. And grows. Aaand grows. And then you encounter a behemoth unseen by your eyes before.
The problem: The if-else pyramid
I've seen code like this in a few codebases now. A verbose nightmare of if-else clauses, often spanning over several rows.
if(userRole === "admin" && isAuthorized) {
// do something for admin
} else if (userRole === "user" && isAuthorized) {
// do something for user
} else if (userRole === "admin" && !isAuthorized && color === "blue") {
// do something for admin with their color set to blue
} else if (userRole === "user" && !isAuthorized && color === "red") {
// ... you get the idea
}
What seems to offer remedy are nested if-statements
if(userRole === "admin") {
if(isAuthorized) {
// Do something for auth admin
} else {
// Do something for unauth admin
}
} else if (userRole === "user") {
if(isAuthorized) {
// Do something for auth admin
} else {
// Do something for unauth admin
}
}
But imagine these checks get more complex and more nested. You start having a hard time tracking what part of the flow control you are in.
if(userRole === "admin") {
if(isAuthorized === true) {
if(color === "red") {
if(loginCount > 5) {
// Do something for auth admin with color red with more than 5 logins
} else {
// Do something for auth admin with color red with less than 5 logins
}
// Do something for auth admin with color red
} else if (color === "blue") {
// Do something for auth admin with color blue
}
} else {
// Do something for unauth admin
}
} // I'll leave the else out here for readability
The above code poses several problems at once
- It's verbose and spaghettified. And imagine whole logic code snippets or functions being executed in each clause.
- It's hard on the eyes. Especially for somebody who didn't write it
- Logic is mixed up with the application's state, which can change during development and sometimes runtime
The proposed solution: Javascript (Hash)maps
Unwinding and refactoring these logical labyrinths is challenging. In the end, there is no way around verbosity. But we can tackle points two and three.
Introducing maps
Note while you could use the Javascript
new Map()
structure, we'll start easy by using a simple Javascript object.
Let's start by moving the conditionals of the above function into a new object - one by one:
Layer 1: User role
The first layer is defined by the role of a user. Valid keys are:
- admin
- user
const sequenceMap = {
admin: {},
user: {}
}
Layer 2: User authorization
The second layer checks whether a user is authorized for the resource. Valid keys are
- authorized
- notAuthorized
const sequenceMap = {
admin: {
authorized: {},
notAuthorized: {}
},
user: {
authorized: {},
notAuthorized: {}
}
}
Layer 3: Login count
Imagine for some reason, you want to check how often the user has logged in. Based on that, you execute different logic. Valid keys are:
- loginCountUnderFive
- loginCountOverFive
const sequenceMap = {
admin: {
authorized: {
loginCountUnderFive: {},
loginCountOverFive: {}
},
notAuthorized: {
loginCountUnderFive: {},
loginCountOverFive: {}
}
},
user: {
authorized: {
loginCountUnderFive: {},
loginCountOverFive: {}
},
notAuthorized: {
loginCountUnderFive: {},
loginCountOverFive: {}
}
}
}
Layer 4: Color
On our final layer, we'll check the color of, well, maybe the user? Valid keys are:
- red
- blue
- green
const sequenceMap = {
admin: {
authorized: {
loginCountUnderFive: {
red: {},
blue: {},
green: {}
},
loginCountOverFive: {
red: {},
blue: {},
green: {}
}
},
notAuthorized: {
loginCountUnderFive: {
red: {},
blue: {},
green: {}
},
loginCountOverFive: {
red: {},
blue: {},
green: {}
}
}
},
user: {
authorized: {
loginCountUnderFive: {
red: {},
blue: {},
green: {}
},
loginCountOverFive: {
red: {},
blue: {},
green: {}
}
},
notAuthorized: {
loginCountUnderFive: {
red: {},
blue: {},
green: {}
},
loginCountOverFive: {
red: {},
blue: {},
green: {}
}
}
}
}
Frankly, this doesn't look much better at first glance.
But we just created a literal roadmap of our control flow. Each segment inside the new object is unique and distinguishable.
A convenient side effect: VSCode will provide you with code completions when accessing the sequenceMap
Layer 5(ish): The logic
Let's add something our program can execute. For example a function for
- an admin
- who is authorized
- has a loginCountOverFive
- and a color of green
The segment of this map can be targetted using:
sequenceMap['admin']['authorized']['loginCountOverFile']['green']
The value of this segment will be located here:
const sequenceMap = {
admin: {
authorized: {
/* ... */
loginCountOverFive: {
/* ... */
green: {} // <-- here it is
}
},
notAuthorized: { /* */ }
},
user: { /* ... */ }
}
You can now assign a variable or a function to it. Ideally, these will have the same name for each segment to avoid runtime errors.
In this case, we'll just assign a string and log it to the console.
const sequenceMap = {
admin: {
authorized: {
/* ... */
loginCountOverFive: {
/* ... */
green: "Hello admin. You are authorized, have over five login count. Also, you're green."
}
},
notAuthorized: { /* */ }
},
user: { /* ... */ }
}
console.log(sequenceMap['admin']['authorized']['loginCountOverFive']['green'])
And there we have it. A neat and convenient way to control your flow without if-else statements.
Optimizations
Before using the above in production, you might want to consider the following points:
- Exception handling for missing segments
- Encapsulation
- Freezing the sequence map
Exception handling for missing segments
Trying to access a segment that does not exist will throw a type error. In classical if-else or switch statements, you would want a default value. Sometimes for each segment separately.
Handling missing segments is crucial for debugging the sequence map later!
If you need just a single fallback case for the whole map, you can use the ternary operator ?
when accessing the sequence map.
console.log(sequenceMap['a']?.['b']?.['c']?.['d'] || "Role sequence not recognized")
When you need a fallback case for every single segment, it gets tricky. In this case, since the final segment always returns a string, we could add a 'default' segment to each layer. And a function to evaluate our sequence map instead of trying to access it directly:
const sequenceMap = {
admin: {
authorized: {
/* ... */
loginCountOverFive: {
/* ... */
green: "Hello admin. You are authorized, have over five login count. Also, you're green.",
default: "Color not recognized"
},
default: "Login count not recognized"
},
notAuthorized: { /* */ },
default: "Authorization not recognized"
},
user: { /* ... */ },
default: "User level not recognized"
}
function evalSequenceMap(userLevel, authorization, loginCount, color) {
return !sequenceMap[userLevel] ? sequenceMap['default'] :
!sequenceMap[userLevel][authorization] ? sequenceMap[userLevel]['default'] :
!sequenceMap[userLevel][authorization][loginCount] ? sequenceMap[userLevel][authorization]['default'] :
!sequenceMap[userLevel][authorization][loginCount][color] ? sequenceMap[userLevel][authorization][loginCount]['default'] : sequenceMap[userLevel][authorization][loginCount][color]
}
console.log(evalSequenceMap('admin', 'authorized', 'loginCountOverFive', 'green'))
console.log(evalSequenceMap('admin', 'authorized', 'loginCountOverFive', 'blue'))
console.log(evalSequenceMap('admin', 'authorized', 'someCount', 'green'))
console.log(evalSequenceMap('admin', 'Someauth', 'loginCountOverFive', 'green'))
console.log(evalSequenceMap('developer', 'authorized', 'loginCountOverFive', 'green'))
Encapsulation
Sequence maps like this can become massive. It makes sense to put each sequence map into a separate folder and export it as JSON or a Javascript object.
If you're using OOP, you could also create a public class that handles the state and can access its values in a convenient manner.
class Authorizer {
constructor() {
this.sequenceMap = {
admin: {
authorized: {
/* ... */
loginCountOverFive: {
/* ... */
green: "Hello admin. You are authorized, have over five login count. Also, you're green."
}
},
notAuthorized: { /* */ }
},
user: { /* ... */ }
}
}
getGreenAuthAdminWithManyLogins() {
return this.sequenceMap['admin']['authorized']['loginCountOverFive']['green']
}
}
console.log(new Authorizer().getGreenAuthAdminWithManyLogins())
Freezing the sequence map
Javascript objects are extendable by default. Our sequence map is no different. This makes it prone to unexpected changes by other devs or malicious prototype pollution.
To prevent this, you can use Object.freeze(sequenceMap)
. It will cause possible changes to the object to silently fail.
Object.freeze(sequenceMap);
sequenceMap.admin = {
admin: "Hello"
}
console.log(sequenceMap['admin']['authorized']['loginCountOverFive']['green']) // <-- (still works)