Escaping From the Pyramid of Doom

Posted at — Dec 19, 2020
#arrow-shape

Escaping From the Pyramid of Doom

When we’re using many nested if statements (that is a lot of indentation), your codebase looks messy and hard to read, a situation like this is commonly called the Pyramid of Doom.

First time I faced the Pyramid of Doom when I do my homework. My lecturer gave me several problems, one of them is:

When we shop at supermarkets, our total spending is often not a multiple of the applicable rupiah denomination. For example, the total expenditure value is IDR 19,212. If the lowest current rupiah denomination is IDR 25, IDR 50, and IDR 100. Usually, supermarket cashiers round up the shopping value to the most considerable fraction. For example, rounds IDR 19,212 to IDR 19,225. Of course, it’s detrimental to consumers. Suppose you have an honest supermarket that does not harm the buyer. So, if there is a purchase value that is not a multiple of the existing fraction, then it rounds down to the lowest fraction. So, rounds IDR 19,212 to IDR 19,200.

Let’s solve that problem!

Creating calculateChange function with totalPrice parameter and return the change as an integer.

Here’s the code:

func calculateChange(totalPrice: Int) -> Int {
    var change = totalPrice

    let f1000 = change / 1000
    let f100 = (change % 1000) / 100
    let f50 = (change % 100) / 50
    let f25 = (change % 50) / 25

    if f1000 != 0 {
        if f100 != 0 {
            if f50 != 0 {
                if f25 != 0 {
                    change = (f1000*1000) + (f100*100) + (f50*50) + (f25*25)
                } else {
                    change = (f1000*1000) + (f100*100) + (f50*50)
                }
            } else {
                if f25 != 0 {
                    change = (f1000*1000) + (f100*100) + (f25*25)
                } else {
                    change = (f1000*1000) + (f100*100)
                }
            }
        } else {
            if f50 != 0 {
                if f25 != 0 {
                    change = (f1000*1000) + (f50*50) + (f25*25)
                } else {
                    change = (f1000*1000) + (f50*50)
                }
            } else {
                if f25 != 0 {
                    change = (f1000*1000) + (f25*25)
                } else {
                    change = f1000*1000
                }
            }
        }
    } else if f100 != 0 {
        if f50 != 0 {
            if f25 != 0 {
                change = (f100*100) + (f50*50) + (f25*25)
            } else {
                change = (f100*100) + (f50*50)
            }
        } else {
            if f25 != 0 {
                change = (f100*100) + (f25*25)
            } else {
                change = (f100*100)
            }
        }
    } else if f50 != 0 {
        if f25 != 0 {
            change = (f50*50) + (f25*25)
        } else {
            change = f50*50
        }
    } else if f25 != 0 {
        change = f25*25
    } else {
        change = 0
    }

    return change
}

func calculateF25(f25: Int) -> Int {
    guard f25 != 0 else {
        return 0
    }

    return f25*25
}

I’ve been using a lot of nested if statements. Exactly, the algorithm to solve that problem is so simple but the code is complicated to understand what is happening.

As you can see, that nested if statements look like Pyramid.

There are many different ways to escape or avoiding the Pyramid of Doom. In this case, I’ll use some function to refactor my code.

So, let’s refactor our code into separate functions.

func calculateChange(totalPrice: Int) -> Int {
    let f1000 = totalPrice / 1000
    let f100 = (totalPrice % 1000) / 100
    let f50 = (totalPrice % 100) / 50
    let f25 = (totalPrice % 50) / 25

    if f1000 != 0 {
        return calculateF1000(f1000: f1000, f100: f100, f50: f50, f25: f25)
    } else if f100 != 0 {
        return calculateF100(f100: f100, f50: f50, f25: f25)
    } else if f50 != 0 {
        return calculateF50(f50: f50, f25: f25)
    } else {
        return calculateF25(f25: f25)
    }
}

func calculateF1000(f1000: Int, f100: Int, f50: Int, f25: Int) -> Int {
    return (f1000*1000) + calculateF100(f100: f100, f50: f50, f25: f25)
}

func calculateF100(f100: Int, f50: Int, f25: Int) -> Int {
    return (f100*100) + calculateF50(f50: f50, f25: f25)
}

func calculateF50(f50: Int, f25: Int) -> Int {
    return (f50*50) + calculateF25(f25: f25)
}

func calculateF25(f25: Int) -> Int {
    guard f25 != 0 else {
        return 0
    }

    return f25*25
}

Now our code is clean enough! There is no more a lot of indentation, the function has only one responsibility, and the lines of code are less than before.

Conclusion

At first, we might think that our code can’t be refactored anymore. But it’s not like that, the code that we believe is clean can still be refactored. Maybe if we consider from another perspective, our last code can be improved to make it even better.