After writing code for over ten years, I’ve found a process that lends itself well to writing code and writing it well. These three simple steps are an approach to writing code that I wish I learned and adopted earlier.
1. Make It Work
Code that doesn’t compile is useless. Similarly, code that looks nice but doesn’t accomplish the task at hand is arguably equally useless. That’s why the first step in the process is always making your code work. Too many engineers get bogged down in doing things “correctly”. While correctness matters before checking in changes, it shouldn’t be your first goal when writing them.
To help ensure focus on making things work initially and nothing else it’s not uncommon for me to write down a list of items I’d like to come back to as I code. Certain code should be wrapped in a helper method? Write it as a to-do on the list. A hardcoded string should be made into a constant? Another item gets added to the list. I use the list to help prevent myself from going down other related rabbit holes or “side quests” while accomplishing the initial task I set out to do.
While it seems easy enough to do as you go along, it’s not uncommon for one “simple” task to lead to two others which each fan out to three more etc. Before you know it, you’re several levels deep refactoring and restructuring existing code all in the name of the one-point ticket you picked up out of the backlog. But don’t worry, soon enough you’ll address all the items on your list that are inevitably pestering you, but not before making your code “right”.
2. Make It Right
Once your code works, it’s time to make it right. By this, I mean writing tests to guarantee that your code works as desired and not just in the case of the “happy” path. This step is all about ensuring the correctness of your code. Not only does it provide the functionality you’d expect but it also handles edge cases gracefully and in the preferred fashion.
One great tool to help accomplish this is mutation testing. Mutation tests are used to measure the quality of the tests that you write. They do so by modifying small portions of existing code that your tests rely on. This modified code is referred to as a mutant. For example, one mutant might modify a conditional in your source code, if(condition_one && condition_two)
to be if(condition_one || condition_two)
.
Ideally, modifying a condition as such would cause an existing test to fail. If it doesn’t, you can introduce an additional test case to your codebase to ensure it does, effectively squashing the mutant. While squashing mutants can be tedious, providing confidence in your test suite is essential — tests are only useful when they fail.
3. Clean It Up
You’ve made your code work, and “proved” its correctness with robust test cases, and now you’re ready to clean it up. Now that you have tests, you can refactor freely without the fear of inadvertently modifying existing behavior. This now provides you with the opportunity to revisit the possibly extensive list of to-dos that you gathered while making your code work in step one. Feel free to wrap duplicated code blocks in methods, rename variables, migrate logic to utility files, and more.
Go through the list and check off each item one by one. While it’s now possible that an initial task you jotted down expands to become multiple, it’s okay since you accomplished your overarching goal — now you’re just side-questing. Just be sure to always rerun your test suite early and often as you continue to refactor. The sooner you know you accidentally broke a test the faster you’ll be able to get back to green.
These three seemingly trivial steps have helped me write a lot of code effectively. I hope they can do the same for you.
Drop a like ❤️ and comment below if you made it to the end of the article.
“make it work, make it right, clean it up” ... thanks for sharing Kevin
Love this, Kevin. Reminds me of the quote from Kent Beck, “Make it work, make it right, make it fast.”
I’m not sure if you heard it before this article but if not it’s cool the conclusion is so similar