It’s never too late to make your code better.
A few years ago I was working on a fairly large project that involved our checkout flow and, specifically, what would happen afterward. The project was sizable enough that a few other engineers were working on it as well, and we had divided up the work so that we could integrate it together without stepping on each others’ toes.
During a technical discussion towards the end of the project, we noticed that one of the domain objects I had written could have been given a better name. I was grateful for the feedback. After all, naming things is notoriously hard. Changing the name of an object or two is a relatively minor refactoring, so I happily noted that I would make the change. That’s when the dev lead responded in a way that was surprising to me:
That ship has sailed.
I was really confused for a moment, then deflated. I could have done something better, it was called out in front of the team, then I was essentially prohibited from fixing it. We hadn’t actually shipped anything, we were still developing the software. Of course, we should still be making the code better, I thought.
So, then, why the resistance? In truth, the dev lead was afraid of refactoring. We were engineered into a corner. We had few tests and a lot of messy code. Because of that, if we changed anything we risked breaking everything. Any changes would require a full set of manual regression tests by the QA team. Even still, there was a good chance we wouldn’t know about bugs until they got into production.
In that environment, virtually any change was considered objectively bad. It introduced risks and costs, so the benefits had to be big. Better names just weren’t impactful enough. But big changes were even harder to introduce for the same reasons, the risks were even greater. Our only opportunity to refactor was while we were writing the code. Once committed, it may as well have been written in stone. (It didn’t help that this particular team lacked any meaningful, standardized code review, but that’s another story.)
Most of us are familiar with refactoring. We want to make our code better, faster, cleaner. We’ve all heard of red-green-refactor. We know that our code isn’t done until it’s clean. Yet we often neglect that last step. Change is risky. Deadlines are approaching. The temptation to “call it good” and move on is always there.
Don’t give in. This is, in my view, what separates junior developers from senior developers. Senior developers tend to understand that cutting those corners will only come back to bite them.
We can choose to behave a certain way while writing software. A set of values which, if embraced and ingrained in our everyday practice, will make confident refactoring a natural part of our workflow.
As would later come to know, that dev lead was wrong.
The ship has not sailed.
Practicing TDD means that we are constantly refactoring. We don’t have a choice. TDD requires that we write the minimum code necessary to make the unit tests pass, then write the smallest test we can that will fail. As we go through this cycle we’ll see many small refactorings, and we’ll make them seamlessly and with confidence, empowered by the test suite at our backs.
When our feature is functionally complete, we’ll be able to step back and make any refactoring we want with equal confidence. We can give variables clearer names, extract logic, introduce design patterns or polymorphism, or whatever else seems right.
The practice of building software together with another person tends to make us refactor our code more consistently. When we’re writing code alone, it feels like we’re only letting ourselves down if we don’t go back and simplify that huge method. But when we have a peer sitting right next to us, we’re much less likely to let that slide. If nothing else, we don’t want a colleague to see us writing poor quality code.
Meaningful Code Reviews
There are many ways for code reviews to be effective endeavors that lend themselves to improving code, “lgtm” is not one of them. Code is malleable and, if well tested (see above), easy to change. Code reviews should reflect that. Constructive thoughts about clarity, design, and structure should be shared during code review. There should be an expectation that code will be changed as part of the process, especially if it was not developed using pair programming.
Professional Commitment to Quality
Regardless of any of the above, the only way to ensure that our code gets clean comes from within us as professionals: an unwavering commitment to the quality of the code that we write. Our code isn’t done until it’s clean. We include this time in our estimates. We don’t tell our PM that we can still make the deadline if we skip tests or refactoring anymore than NASA would tell the President they can still make the launch date if they just skip the safety checks.
It won’t save the time we convince ourselves it will. Beyond that, we’re releasing software that will reflect on us long after we’ve left the team. Our professional reputation is vastly more important than the next deadline.
The project that I described earlier would have been much better served if I had approached it with the professionalism I advocate here. If I had properly covered my code with tests, I could have made that name change without a second thought.
But, if not for experiences like this one, I’d still be writing code resigned to an inability to make it better. Accepting spaghetti as the inevitable result of development.
Today, I go so far as to have a checklist of common smells I review before creating a pull request (e.g., am I checking for nil everywhere?). I run static analysis tools like Reek against my code to look for opportunities to clean it up. But most of all, I simply commit to clean code.
How do you make sure you leave your code clean? How about your team?
As always, thanks for reading.