Testing versus code layout
Here is a simplified version of function I once wrote:
if(foo() && bar())
return true; // common case
else if(foo())
return false; // happens too...
else if(mumble() == jumble() && bar())
return true;
else if(mumble() == jumble() && rotten())
throw new MalevolentClientException( ... );
else if(mumble() == null)
return true;
return false;
The real foo() was something along the lines of getRequest().getClientIpAddress().isRoutable(), so not awfully cheap, but it was O(1).
As you've noticed, foo() may be called twice even though the second return value has to be the same as the first. The function could be more efficient:
if(foo()) {
if(bar())
return true;
return false;
} else if(…
There's a reason I wrote the code as I did instead of using more complex if() machinery to call each of foo() and bar() just once: each if/return pair matches one unit test exactly and I was strongly focused on correctness. There were seven if/return pairs and seven unit tests, and because of the ra-ta-ta-ta layout I was confident that they corresponded and that the tests covered 100% of the implementation.
Three people reviewed the code, all of them above-average programmers. None of them noticed the correspondence, all of them disliked the repeated call to foo(), and when I mentioned test/code correspondence, none of them thought that was reason enough to call foo() once more than strictly necessary.
It's an odd way to lay out a function. Most of us would use nested if() without thinking twice, like a flowchart with a call to foo() in the top left box.
I see the appeal of the flowchart-like function. It suits a programmer's mind well. Most of us are fantastically good at understanding hierarchies and structure. We stare at the screen, build a flowchart with basic blocks in our minds, and grok the code. But I've thought about it a few times now (this happened two years ago) and have decided that I prefer the unconventional variant.
The correspondence between code and test is the weak link in many programs: We programmers have fine automatic tools to check that a function uses all its arguments, throws only the right exceptions, returns the right kind of output, and we have fine automatic tools to run tests, but we don't have much to check that the tests are the right ones. That they have good path coverage and requirements coverage. So I generally want to optimise for having simple problems with few variants and edge cases, and simple mapping beteween tests and code. Seven if/then clauses with a minimum of shared logic, seven tests: Great.
At this point, you might be muttering that I'm talking about test-driven code layout. Good point. I am doing that. I didn't realise until I started writing this paragraph, but I am doing just that and I will rename the file before I publish. And the point I'm trying to make is that test-driven development works best if testing is allowed to dominate the way the code is written. Including its layout and minor performance issues.