Readplace

The Code You Write Should Only Suck Less Than The One Before

fagnerbrack.com 7 min read
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

Two side by side pictures of a hotel’s bedroom. In the left, there’s a bed with no pillows, too much light in the environment, one couch, and a cover full of details. In the right, there’s the same bed, only now with two pillows, a better light in the environment, no couch, and a cover without details. The second bedroom setup looks more elegant than the first one.

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":

The code that represents the first three tests for the function “interest to pay for cents” of the unpublished post. All of them expect zero interest. Their input is 0 cents, 100 cents and 200 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.

The code that represents the result of what the initial tests drove me to write. There are two conditions. The first condition runs the algorithm for the first range if the "loan amount in cents" is greater than or equal to $0 and less than $2001. The second condition runs the algorithm for the second range if the "loan amount in cents" is greater than or equal to $2001 and less than $5001.

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:

The code that contains two conditions. The first condition runs the algorithm for the first range if the "loan amount in cents" is less than or equal to $2000. The second condition runs the algorithm for the second range if the "loan amount in cents" is less than or equal to the global property "Infinity."

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.

The code for the body of the second condition that contains the check for "loan amount in cents" greater than or equal to Infinity. The code loops through the result of "5001 minus 2001," which is 3000. For every index, it assigns the result of 9 cents multiplied by the current index and stores that result in an object with the name of "result for overflown cents." In the end, it loops through all the items of "result for overflown cents." For every item in the loop, the code checks if it's the same as the current index plus 2000, which represents the end of the first checkpoint. If there's a match, return the "result for overflown cents" for that "overflown amount."

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.

The diff that shows the transformation of the constant 5001 to Infinity.

So this is what would happen:

The image the browser Google Chrome shows when the tab crashes. It's a dark background with a pixelated sad face in the middle.

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.

The code that contains three conditions. The first condition runs the algorithm for the first range if the “loan amount in cents” is less than or equal to $2000. The second condition runs the algorithm for the second range if the "loan amount in cents" is less than or equal to the “loan amount in cents” minus $2001. The third condition runs the algorithm for the third range and onwards if the "loan amount in cents" is less than or equal to Infinity. The third condition runs a mathematical calculation that divides the "loan amount in cents" by 100 to get the dollar amount, subtracts $2000 and then multiplies the difference by 9 cents.

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:

A JSFiddle that represents the running code for the unpublished solution.

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.