Else, Switch, And Map
Back when I was in 7th grade I entered some kind of scholastic programming competition. No idea what it was called, or what most of the tasks were. I do remember it was 3 hours on a Sunday afternoon, we were using Basic on a TRS-80, and one of the tasks was to come up with a frequency chart of characters in a block of text. The code I submitted looked a lot like
1 DIM A as string
2 DIM I as integer
3 DIM C as string
4 DIM N(26) as integer
5 INPUT A
6 FOR I = 1 to LEN (A)
7 C = MID(A, I, 1)
8 IF C <> "A" AND C <> "a" THEN GOTO LETTER_B
9 N(0) = N(0) + 1
10 LETTER_B:
11 IF C <> "B" AND C <> "b" THEN GOTO LETTER_C
12 N(1) = N(1) + 1
13 LETTER_C:
14 IF C <> "C" AND C <> "c" THEN GOTO LETTER_D
15 N(2) = N(2) + 1
°
°
°
82 LETTER_Z:
83 IF C <> "Z" AND C <> "z" THEN GOTO NEXT_I
84 N(25) = N(25) + 1
85 NEXT_I:
86 NEXT I
87 FOR I = 0 to 25
88 PRINT "There were"; N(I); " "; CHR(65+I); "'s"
89 NEXT I
Hey, It's ugly, but it worked, or at least it mostly did. Lots of copypasta and lots of copypasta errors. In
the comparisons, the indices, and the GOTOs. But that was early in my career. Not long after that I realized I
could have just used the ASC() function to get the ascii character and use that (suitably adjusted) as the
index. So all those sequential IFs turn into a simple set of assignments.
With a more modern language a map of character or string to integer would be even easier. The body of the loop
turned into N[ToUpper(C)]++
Much easier to read, much less error-prone to type, and handles
things that aren't alphabetic characters to boot. My original attempt didn't crash on numbers or punctuation,
but it didn't count them either.
The point of this isn't that my code wasn't very clean 40 years ago (it wasn't), but that while you can take a
very procedural approach to coding, a better choice is almost always to let the data and data structures guide
you. Cascading if
's are rarely a good idea. For smaller numbers of choices consider a
switch
. That can make things a lot easier to read at least.
For multi-dimensional things, you could do nested switch
es, methods. One approach I like in such
cases is a multi-dimensional map.
Let's say you need to calculate something which is a function of color, width, shape, and language. You could
have one function with all of the inputs and a bunch of internal logic, some of which might be
very different (shape/language differences). You could come up with a class hierarchy that handles
it, create the classes, make a factory, and use it. Or, write the functions that are different, use the inputs
and a map, and decide which one to call. In pseudo-code something like
map[shape][language](func (c color, w linewidth, s shape, l language))
, and use
calculator := map[shape][language] {
{CIRCLE, ENGLISH, CalcCircleRomance},
{TRIANGLE, ENGLISH, CalcTriangleRomance},
{CIRCLE, FRENCH, CalcCircleRomance},
{TRIANGLE, FRENCH, CalcTriangleRomance},
{CIRCLE, ITALIAN, CalcCircleRomance},
{TRIANGLE, ITALIAN, CalcTriangleRomance},
{CIRCLE, RUSSIAN, CalcCircleCyrillic},
{TRIANGLE, RUSSIAN, CalcTriangleCyrillic},
{CIRCLE, BULGARIAN, CalcCircleCyrillic},
{TRIANGLE, BULGARIAN, CalcTriangleCyrillic},
}
func := calculator(shape, language)
if (func == null) {
throw Unsupported
}
result := func(color, width, shape, language)
...
With this pattern the decision logic is collected in one place and it's clear what each pair of shape/language is going to do. The list of supported pairs is easy to see, and it's easy to extend. All of which tends to reduce cognitive load. Which, as I've mentioned before, is a good thing.
Obviously you can take this too far, and at some point a polymorphic hierarchy makes sense. But if you don't need that complexity this is a good compromise.