/ READABLE CODE, SOFTWARE DEVELOPMENT

Even more readable code without if-else

A couple of years ago, I wrote a post focused on how to avoid sequences of if-else statements. In that post, I demo several alternatives:

  • the usage of proper OOP design
  • maps
  • when there’s no return, switch statements in a case.

Recently, I stumbled upon a slightly more complex use-case. This post describes it, and details what additional options are available in Kotlin.

Modeling a simple if…​ else sequence

Let’s start with modeling a simple sequence of if-else statements. Imagine we need to check the value of a letter to return an integer associated with the later.

As mentioned in the previous post, an easy alternative to if-else is to create a map, with letters as keys, and integers as values.

val mappings = mapOf(
  "A" to 1,
  "B" to 2,
  "C" to 3,
  "D" to 4,
  "E" to 5,
  "F" to 6,
  "G" to 7
)

val result = mappings["A"]
Remember to always use immutable types for keys!

Making the problem more complex

Now imagine that instead of checking one value, one needs to check two different inputs to compute the return value. The straightforward way to solve this is with embedded if:

if (input1 == "A") {
  if (input2 == "A") return 1
  if (input2 == "B") return 2
  if (input2 == "C") return 3
  // and the rest
} else if (input1 == "B") {
  if (input2 == "A") return 2
  if (input2 == "B") return 4
  if (input2 == "C") return 6
  // and the rest
}
// and the rest

With the maps approach, this would be akin to:

val mappings = mapOf(
  "A" to mapOf(
    "A" to 1,
    "B" to 2,
    "C" to 3
    // and the rest
  ),
  "B" to mapOf(
    "A" to 2,
    "B" to 4,
    "C" to 6
    // and the rest
  )
  // and the rest
)

val result = mappings["A"]["B"]

I believe the previous code is hard to read:

  • Creating the map requires a lot of nested code
  • Getting the result is a two-step process - first get a one-dimensional map, then the scalar value
  • The order of the keys shouldn’t be mixed up: mappings["A"]["B"] is (probably) different from mappings["B"]["A"]

Solving the complex problem

Let’s stop for a second, and model the first sequence as an array:

A B  C D E F G

1

 2

3

4

5

6

7

The above solution has one dimension. Imagine now that the choices are a 2D matrix:

A B  C D E F G

A

1

 2

3

4

5

6

7

B

2

 4

6

8

10

12

14

C

3

 6

9

12

15

18

21

D

4

 8

12

16

20

24

28

E

5

 10

15

20

25

30

35

F

6

 12

18

24

30

36

42

G

7

 14

21

28

35

42

49

This is akin to a multimap, a map with two keys. Neither Java nor Kotlin has a Multimap type, but libraries do:

  1. For example, there’s one in Guava
  2. And another one in Apache Commons Lang

With Kotlin, there’s no need for a multimap, up to 3 dimensions, thanks to the Pair and Triple classes.

val mappings = mapOf(
  ("A" to "A") to 1,
  ("A" to "B") to 2,
  ("A" to "C") to 3,
  ("B" to "A") to 2,
  ("B" to "B") to 4,
  ("B" to "C") to 6
  // and the rest
)

val result = mappings["A" to "B"]
For keys with a dimension higher than 3, create a dedicated immutable type, and override its equals() and hashCode() methods.

Returning computations

In the old post, a switch was used when the code didn’t return a result, but invoked a method instead:

when(key) {
  "A" to "A" -> doSomething()
  "A" to "B" -> doSomethingElse()
  "A" to "C" -> andNowForSomethingCompletelyDifferent()
}

However, it’s an even better alternative to store the computation in the map. It can then be invoked dynamically:

val mappings = mapOf(
  ("A" to "A") to { doSomething() },
  ("A" to "B") to { doSomethingElse() }
  ("A" to "C") to { andNowForSomethingCompletelyDifferent() }
)

mappings["A" to "B"]?.invoke()

The only requirement is that all computations have the same signature: here, a function that takes no input parameter.

The return value is not used in the above code. If it does, it can be captured easily:

val result = mappings["A" to "B"]?.invoke()

If a default value is needed when no key matches, the getOrDefault() function is your friend:

val mappings = mapOf(
  ("A" to "A") to { doSomething() },
  ("A" to "B") to { doSomethingElse() }
  ("A" to "C") to { andNowForSomethingCompletelyDifferent() }
)

val result = mappings.getOrDefault("A" to "E") { doDefault() }.invoke()

Conclusion

In the earlier post, I listed some simple techniques to avoid sequences of if-else statements that were relevant for simple use-cases. In this one, I showed more advanced techniques that adress more advanced usages: multimaps for nested evaluations, and storing the computation as a value in the map when code execution is required.

Improving code readability should be a number one priority when writing code. It doesn’t require fancy techniques, just a constant drive to achieve it. Knowing and understanding one’s language and available libraries goes a long way toward that goal.

Nicolas Fränkel

Nicolas Fränkel

Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
Even more readable code without if-else
Share this