I have done another refactoring session in my last two blog posts. I have covered refactoring in other blog posts, but there is a thing that is really important about refactoring: Knowing what to refactor and what to leave as it is for now.
Know when to stop
I got a few comments on the first and second part of my last refactoring session suggesting next steps. While some of those suggestions are reasonable, there are others I would not consider, for different reasons.
One of the most important reasons to stop refactoring is that our code will never ever be perfect. We can make it more readable, shorten the functions, build a beautiful class design – but we will always find something to improve.
We also should stop refactoring if we are not sure about the way our code is supposed to be used or designed. This happens especially if we are working with large legacy code. Of course this does not apply to the more trivial refactorings like expressive variable names. However, if we don’t know what our code does or how it is used, we can not be sure how to design it best.
In code bases that are large or simply very “dirty” we often find so many issues that we don’t know where to start. If we just start to refactor everything we see we may spend hours, days and weeks refactoring code without having a huge impact on the overall code base.
In such cases, it is best to have a plan what to refactor and to what goal. If we want to deliver some value with our refactored code, we should stick to that plan.
Constrain the location
If we want our refactorings to have impact, we should start at the places that are changed most. Those are the places where clean and readable code matters most, because developers have to read it often. Nobody cares if some utility package that just works and has never been touched for years looks clean or not.
There is a really nice article, book and talk video by Adam Tornhill: “Your code as a crime scene“. He shows some really nice techniques to pin point hot spots in your code base where refactoring makes most sense.
Know when not to stop
Of course there always is the possibility of just not doing enough. For example, in the last refactoring there was a single step I should have done to finish what I had started. Sometimes that last missing piece can mean the difference between a successful and a almost useless refactoring.
Constrain the goal
Even after we have found some hot spots, we often end up with a handful of really large files. If we want to be done with the refactoring any time soon, we still can not hope to deal with every issue we encounter.
Therefore it is best to set out to fix a single class of issues at a time. That means if we fix the names of our functions and variables, we should do just that, nothing else.
Here are some examples of goals to pick and strick to:
- Rename functions and variables that are poorly named
- Factor out functions and even classes if the existing granularity is too coarse
- Make a set of classes and function const correct
Constrain the time
Especially a refactoring const correctness can still get out of hand quickly. We add const to a function, and the compiler complains about some of the functions it calls. It may also complain about some of the places where the refactored function is called. We change those other places, and end up with a single change rippling through the whole code base.
In such cases, i.e. if we find that we can compile a change only after a whole lot of other changes, it is best to abort that refactoring. Roll back the change and start with the prerequisites. If those turn out to require even more changes, roll back again. Dig deeper until you find changes that you can do in time.
When having a time box set it is crucial to stick to it. I am not talking about five or ten minutes here, but if you do not see an end or if that end just stays out of reach again and again, stop. Roll back your changes and split the task in smaller, more manageable refactorings.