This is becoming one of my favorite mantras: “refactor or die.”
Recently, I’ve once again put into practice one of the best benefits of doing this kind of work continuously. The end result is very satisfying, even after plenty of moments like “this is turning out terribly,” “I’m going to end up with a tangled mess of a solution this way,” and so on. It’s easy to get discouraged, especially when it’s a small project you’re working on in spare moments at night or on weekends.
Still, a little perseverance (and probably throwing everything away and starting over at some point) gets you to a good solution that not only works, but is also relatively easy to extend and evolve.
People talk a lot about productivity. To me, it’s very simple to describe what is and isn’t productive in software: simple solutions are more productive than intractable ones only their authors can understand; anything that lets us develop faster is more productive, because it helps us get more done with less effort. That’s it. Simple and emphatic.
Some people try to improve one aspect of an application’s code at some point in the process: at the end, once everything works, or every now and then… Yet the benefits of incorporating these improvements continuously aren’t always seen as productive—especially when deadlines are getting dangerously close and we start leaving loose ends (which we’ll certainly remember weeks or months later).
Why refactor when the important thing is that an application works for the client? That’s a good question, and a naive one at the same time. Anyone who still doesn’t see the benefits of investing time in this work should know that the first thing you gain is development speed, and therefore productivity.
The speed at which we develop any piece of software is a measure of productivity, especially when it brings us closer to delivering the functionality we’ve been asked for.
We should aim for the design or structure of the solution we’re building to help us move faster over time, not slower.
In the application I’ve been working on for a while, I’ve continuously applied every improvement technique I could: simplifications, abstractions into utility classes, improvements to the solution’s structure, modularization, and so on. As a result, every two weeks I’ve seen that the code in a commit from fifteen days earlier has almost nothing in common with the latest one. This matters especially when, from the beginning, we’re not sure how to implement some of the more complicated parts of the application.
As we move forward and apply all this improvement work, we see a better design gradually emerge. “Better” is a very subjective word and difficult for two or more people to agree on. To me, in software, something is better if it lets you develop faster (spend less time on the same solution) without losing quality in any of its aspects.
That elegant, simple, sound design is what lets us gain speed after a certain point. Eventually, you realize that all that improvement work was actually a good investment of time that is now paying off. It’s like having a tangled ball of yarn that’s very difficult to unwind at first, until it becomes easy and quick to pull out more and more.
In the particular project I’m working on, I’ve reached the point where I’ve solved all the more difficult parts so simply and elegantly that all that’s left is to polish the rest and finish the application. I couldn’t have done that if I hadn’t paused at the beginning to improve the application in every way I could.
The early stages of an application with many unanswered questions are tough. It’s hard to see real progress, and we’re tempted to take the quick and easy route without realizing that it will work against us over time.
Eventually, the best architecture and design for our purpose becomes mature enough that all we need to do is follow that coherent direction to finish the application successfully.
Best of all, we’ll end up with a clean solution that’s easy to maintain and evolve.
Some of the techniques I’ve used so far include:
- As the code grew, I kept adapting and improving its structure and organization.
- I broke long classes down into more specific, simpler classes with a logical relationship between them (the SRP).
- I properly isolated duplication into the appropriate services (the front-end is built with AngularJS).
- I kept looking for anything that could be injected—that is, dependencies that could be extracted to implement dependency injection.
- I simplified many similar blocks of code with corresponding utility functions.
- I simplified methods with more than three or four parameters.
- And much more.
Ultimately, this kind of work is training. You end up doing it naturally and no longer consciously think of these activities as separate from development work, but as an inherent part of it.