In this blog post I will try to explain some ideas that I try to keep in mind when I am maintaining any piece of software. By no means I am a coding guru, but I have noticed that the quality of my code deliverables increases when I follow certain procedures.


Software development is difficult.

As a project grows bigger, its inherent complexity grows as well. Following the paradigm properly (e.g., abstraction, inheritance, encapsulation, etc. in object-oriented programming) helps manage the complexity of the project, when done properly.

Successful software projects are often evolved and maintained over years, involving new developers and letting other developers go. Over time, new features are introduced, others are removed, and fixes and enhancements are developed all over the place. Software is alive.

TDD

I have been following Test-Driven Development pretty much my entire career. There are plenty of resources out there, so I will not go into describing what it is and the benefits of using it.

I find TDD very useful when I am fixing a bug. When I face a bug report, the first thing I try to do is reproducing it manually. When I have verified the bug does exist, I reproduce it in a test. Only then, when I have the test failing, I start working on the actual fix for the bug. Not only this validates that the fix works, it makes sure it is not introduced in the future.

Whenever you write the test for the bug fix after the code has been fixed, there is no guarantee that the test didn’t pass with the previous version of the actual code. While reviewing PRs (in different companies and projects), I have tried running the new test on the old codebase, and surprisingly, sometimes it passed. That means that the test was not actually testing the buggy scenario, so the bug may or may not have been fixed by the actual code change.

The other scenario where I find TDD extremely useful is while developing new code from scratch. When tests are developed first, you are forced to think how would I like to use this class? What should be the minimum interface it should expose? What parameters would be more convenient for these methods? What are the edge cases?. The design is not focused just on making something work, but also on making it usable.

Refactoring

Refactoring is another fundamental piece of software development. When I speak with other developers, I often see what I consider to be some misconceptions of the refactoring term. The way I see it (I could be wrong, though), refactoring consists in applying some procedures to change code that guarantee that the functional behavior of a piece of software remains the same. If there is a functional change, it is not a refactor. It is something else.

There are wonderful books that describe a catalog of such techniques so they can be applied step by step. The beauty of these techniques is that, when followed properly, they guarantee that the final result is functionally equivalent to the starting point.

Of course, it is crucial that the subject of the refactor is properly tested upfront. Whenever I have to work on a piece of code, I always look for the tests first. Often times, I rewrite part or most of the test suite to make sure it accurately reflects what the code is doing. Only then I can be certain that the code will work the same after the refactor. Refactoring code that is not tested is, at the very least, temerarious if not directly unprofessional. I would be lying if I said I have never done it, or I won’t do it again. But when I do it, I know I’m not doing the right thing, and I cannot be proud of it.

Evolving software

Even though I don’t have a formal demonstration, I have the feeling that any task can be developed following a sequence of refactoring and TDD iterations. Safe small steps. The real challenge is finding how to connect the dots to get to the final picture, making sure every little step (dot) is developed without risk.

The bigger the project, the more dots we need to connect, and the more difficult it is to find the way out of the maze. This path is sometimes counter-intuitive, maybe requiring us to create a class from scratch that we know we will remove later. However, that class may be a necessary step to avoid risks. Sometimes we need to work on what seems to be a step backwards, and sacrifice speed for quality. Sometimes we need to write a heavy test suite of an existing class we are going to remove. And that is fine.

Not following this principle works as well. Sometimes. In my experience, gut feeling development often introduces more bugs than procedure-based developments.

I hope this article helps you balance your development practices towards a more procedural development. Happy coding!