Eliminate if-else Statements Hell

  • Hans Huang
  • 60 Minutes
  • October 31, 2020

Like the callback hell, our codes is also suffering from massive if-else statement. You must have ever saw some source codes like below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

fn() {
if (a == 1 || a == 2) {
if(condition_b) {
// ...
} else if (condition_c) {
// multiple level nesting
}
//...
} else if (condition_d){
// if(condition_b) {
// ...
// }
} else if (condition_e) {
// ...
}
}

Usually the if-else statement is added intuitively to support an additional case, or everyone just simply contribute one if during maintenance which finally caused the hell: buggy, complex logic description, low readability, difficult to refactor.

Here is sharing some patterns which helps to eliminate massive if-else statements in our daily coding.

1. Guard Clauses

Sometimes the if-else is added for code defense like below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn (foo, bar) {
if (foo < 0 || bar == null) {
// do some defense or throw error
} else {
// main business logic
}
}

// or whole method body is under "if" protection
fn (arg) {
if (arg != null) {
let foo = bar(arg);
if (foo) {
// business logic
}
}
}

In this case we can apply Guard Clause , leave the the if-else away from business logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn (foo, bar) {
if (foo < 0 || bar == null) {
// log or throw error
return;
}
// ...
}

fn (arg) {
if (arg == null) return;
let foo = bar(arg);
if (!foo) return;

// ...
}

2. Value Extraction

Below code is sample of bad smell, in real world it could be alive and hidden in more complex business logic codes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn (arg) {
if (arg.foo() == foo) {
if (arg.bar() == bar) {
if (arg.moo() != moo) {
return y;
} else {
return x;
}
} else {
return y;
}

} else {
return y;
}
}

Obviously in this case, the codes/value can be extracted as below:

1
2
3
4
5
6
7
fn (arg) {
if (arg.foo() == foo && arg.bar() == bar && arg.moo() == moo) {
return x;
}
return y;
}

or the ternary expression can be applied:

1
2
3
4
5
fn (arg) {
return arg.foo() == foo && arg.bar() == bar && arg.moo() == moo
? x
: y;
}

if the x y stands for boolean, the value could be directly returned:

1
2
3
fn (arg) {
return arg.foo() == foo && arg.bar() == bar && arg.moo() == moo;
}

3. Apply Map or Array

Maybe below case can be improved by switch-case, but also can try an array.

Before:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn (foo) {
let result = "";

if( foo == 0) {
result = "a";
} else if (foo == 1) {
result = "b";
} else if (foo == 2) {
result = "c";
}

return result;
}

After:

1
2
3
4
fn (foo) {
const values = ["a", "b", "c"];
return foo < values.length ? values[foo] : "";
}

Or some cases can be constructed to a map:

Before:

1
2
3
4
5
6
7
8
9
10
fn (foo) {
if (foo == "a") {
// do sth A
} else if (foo == "b") {
// do sth B
} else if (foo == "c") {
// do sth C
}
}

After:

1
2
3
4
5
6
7
8
9
fn (foo) {
const actions = {
a: () => { /* do sth A */ },
b: () => { /* do sth B */ },
c: () => { /* do sth C */ }
}
// ... some defense
actions[foo]();
}

One more tips, for some long if condition could also be optimized by array or map.

Before:

1
2
3
if (foo.bar == "aaa" || foo.bar == "bbb" || foo.bar == "ccc") {
// ...
}

After:

1
2
3
if (["aaa", "bbb", "ccc"].includes(foo.bar)) {
// ...
}

Some idea from functional programming can be applied.

4. Apply Metadata / OOP

Below example can be applied in a more complex scenarios or high level design.

Say we have a render function to return data string by required format, original codes could be like below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn render (data, format) {
let result = ''

if (format == 'json') {
// long/complex logic to generate json
result = //...
} else if (format == 'xml') {
// long/complex logic to generate xml
result = //...
} else if (format == 'html') {
// long/complex logic to generate html
result = //...
}

return result;
}

Maybe for this case we can apply a map as above in #3, but what if the logic here is much complexer and we want to modularize our source codes to improve scalability, so that we can consider OOP to abstract a renderer interface for render class. Multiple different implementation for different format methods, like json renderer, xml renderer, html renderer. And we can tag the implementation classes, allow it self-describe its metadata. The technology here used is called Attribute in C#, Annotation in Java, Decorator in Python/ES2016/TS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

const renderers = new Map()

const tag = format => targetClass => {
rendererMap.set(format, targetClass)
}

@tag('json')
class JsonRender {
render(data) {
// long/complex logic to generate json
}
}

@tag('xml')
class XmlRender {
render(data) {
// long/complex logic to generate xml
}
}

@tag('html')
class HtmlRender {
render(data) {
// long/complex logic to generate html
}
}

fn render (data, format) {
const renderer = rendererMap.get(format)
return new renderer().render(data)
}

It’s exactly satisfied the OCP, once there is requirement for new formatter, just add a new class and zero-touched to existing logic.