We’ve all seen some code that looks a bit like this.
String prefix = input.substring(0,2); String delimiter = input.substring(input.length()/2, 1); String postfix = input.substring(input.length()-2, 2); String result = list.stream() .collect(joining(delimiter, prefix, postfix)); return result;
We were probably taught it was good, because we’re naming local variable and self-documenting what’s going on. I’ve even heard that those local variables are good because they’re places to linger on when you’re debugging.
Quick aside, don’t optimise for debugging, write unit tests that debug your code every goddamned build so you never have to run a debugger yourself!
Let’s look at the above from the point of view of the narrative, though.
- There’s this prefix which is the first two characters of the input
- There’s this delimiter which is the middle character of the input
- There’s this postfix (suffix) which is the last two characters of the input
- The result is the list joined with the things above
- And we return it
The word order of this is unnatural. The temp variables are either hard to name, or named in a banal way which hides the what, but then inlining the what would completely suck.
Moreover, the above method is working at two levels of abstraction – its job is to create a processed result, which it ultimately uses a library for, but on the way it does some deeper more detailed calculations.
Software should read from left to right not requiring the reader to poke facts into their brain to inline into the code later. With this ring I thee wed should be refactored to I am marrying you using this ring. The code should be of the following form:
return list.stream() .collect(joining( , , ));
You read that as – I’m joining the list, with a delimiter, prefix and suffix, the details of which I can think about as I read from left to right.
But how do you do that without the splurge of code that makes it seem monolithic?
The answer is functional decomposition. Don’t give me dodgy variables, give me function calls.
return list.stream() .collect(joining( centralCharacter(input), naturalPrefix(input), naturalSuffix(input) ));
The moment you do this a few things happen:
- You ask yourself what is this algorithm for the prefix
- When you write the functions you’re better able to thing about unit testing them and any of their edge cases (strings shorter than 2 for instance)
- You question why the algorithm is as it is
- You give the reader the opportunity to dig deeper, or not
So, inline your variables, and replace here-and-now calculations with well named functions. Functions are named after their purpose not their implemented.
Warning: the substring function above is probably not correct, but illustrates the point.
To return to the subject of this post – With this ring I thee wed – which of the two forms below seems more natural to read now?
Ring thisRing = new Ring(); me.marry(thee, thisRing); // or me.marry(thee, new Ring());