The Code You Write Should Only Suck Less Than The One Before
View original- current
Summary (TL;DR)
The author shares a cautionary tale about over-engineering a TDD solution. They initially tried to represent cents and dollars separately with the same Number type, leading to complex conditionals and a crashing loop that stored every calculation for infinite ranges. The code became messy and unclear. After hitting a dead end, they refactored to a cleaner solution using fractions (e.g., 1.50), accepting floating-point tradeoffs. Key lessons: use distinct types for different units, avoid storing infinite calculations, and accept that code iteratively improves rather than being perfect on the first try.
The Code You Write Should Only Suck Less Than The One Before
We’ve all written over-engineered, over complicated, messy crap piece of code at some point in our careers.
In my case, that was a few weeks ago.
Note: Last time I wrote a step-by-step example of how to apply Inside Out Test-Driven Development using JavaScript. That post is a requirement for you to make sense of this one.
One thing I didn't tell in the previous post, is that I spiked some code before the publication. I wanted to make sure the post, once published, didn't have all that back and forth from the development to avoid the risk of frustrating the reader. However, that would also mean it would lose part of the learnings you get once you embark in the journey of TDD.
If you write code using TDD, that means you want to discover a better solution for a problem. That's why there’s a lot of back and forth in the development while you’re learning the intricacies of the problem you’re trying to solve.
After the publication, I had two versions of the same solution: the one I published and the one I didn't.
This post is going to talk about the one I didn't.
If you write code using TDD, that means you want to learn a better solution.
For the code I didn't publish, I started early on the coding process with the calculation in cents to avoid floating point issues. The name of the function was "interest to pay for cents":
When I wrote the tests for more than $2000 as a loan amount, it quickly became clear that there was a different algorithm for each range: from $0 to $2000 and $2001 to $5000.
However, when I realized I could have the code supporting $0.09 for all loan amounts above $2000 in the first deliverable, I changed the conditionals to check for Infinity instead:
This way, I could easily add a third condition for the next range.
The second condition wasn't as simple as the published solution. I ended up with a complicated imperative loop that would iterate over the Magic Number 3000. That Magic Number represented all the dollars for the second range between $5001 and $2001.
The code was insane.
However, that would still not achieve the first incremental delivery, which was to apply $0.09 for all loan amounts above $3000. The Magic Number 3000 was still hardcoded in the loop. The next transformation would be to change the constant 5001 to Infinity.
So this is what would happen:
The browser would crash.
The code was instructing the computer to store every calculation for every possible result in memory. That's insane! I reached a dead end. However, I chose to continue.
To solve that crash and allow the code to work for every input, I would have to create another condition that could tackle every loan amount up to Infinity. That code would multiply $0.09 against the difference of the loan amount and the start of the first range.
At this point, things were too complicated. Also, I realized the second loop was redundant, and it could be commented out.
In the end, I learned a couple of things.
I started with cents from the beginning without a proper model. Therefore, it was tough for me to guess if a Number type was representing cents, or if it was representing dollars. You can see all the bad smells of that problem in the divisions and multiplications at the end of the last code example.
I should not use the Number type to represent both units of cents and dollars separately. Humans are pattern recognition creatures, and TDD is a discipline to assist in the discovery of those patterns. If I use the same type to represent different things, I'm hiding the patterns and setting myself up to fail.
Instead, in the previous post, I've chosen the Number type to represent dollars and cents as fractions, rather than separate units, like 1.50. I chose to make it easier to discover the patterns for the problem at the expense of potential floating point issues like 0.10 + 0.20.
Humans are pattern recognition creatures; it's easier to find a better solution if you make it easier to spot patterns in the code.
The other thing I learned is that it's ok to reach a dead end.
It was clear the code wasn't going anywhere, but I followed along to see how far that would go. In the worst case scenario, I would have learned something that would allow me to create a better code next time.
That's what happened. Reaching a dead end is something essential, yet sometimes missed in tutorials, guides and blog posts around the Web:
Your purpose as a programmer is not to write the perfect code the first time you face a problem; it's to write code that sucks less than the one before.
If you're in a company, it's ok to write code that is not perfect as long as it has enough test coverage. TDD allows you to fail safely. However, my first version was messy, overcomplicated, unclean, and probably had bugs, so it doesn't fit that criterion.
See it for yourself:
I learned that I should not take the shortcut of trying to use the Number type to represent both cents and dollar separately. Instead, I should use a proper Money model from the start or use cents as fractions of a number and accepts the floating point tradeoffs.
Also, I learned that I should not store the calculation of interests in memory. The size grows for each dollar above a range up to infinity, and that’s a mistake.
Next time you do TDD, don't expect to get the best answer straight away. As for every skill, you need practice. As you practice, you'll get more velocity and make better decisions. You may even discover a new order of testing or transformations that can take you to a better end.
If you accept the best possible solution is a Utopia and that Continuous Improvement is the path to mastery, interest will compound. However, this time it’s not money that's going to Jack’s pocket.
It's an interest that goes to yourself and your wealth of knowledge.
An interest you should be happy to deal with.