A lot of software engineers, including myself, are passionate about code quality. This striving for a well-shaped codebase, while getting things done could cost one quite a few hours and nerves, though. I'm constantly looking for ways to achieve these two goals without significant trade-offs. Stand by for the current state.
Refactoring is "a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior" according to Martin Fowler. In other words, the goal pursued by this alteration is a well-structured, readable code doing the same thing, but taking new changes more naturally.
The unchanged observable behaviour he mentioned covers both directions, by the way. Clearly everything should work the same way afterwards. Simultaneously we are liberated from putting any bug fixes or performance improvements on the top of it. It could be a pleasant side effect, but not the destination of refactoring. Better not to feed two birds with one scone to decrease the probability of new bugs. At the end of the day it'll save some debugging time.
It applies, even if optimization is the objective of your current mission. Cold-bloodedly improve structure without any performance, but readability in mind. And then, with much better understanding, start with the optimization. It will often be done in less time, than jumping into a badly structured code right away.
Don't dedicate time
Sounds counterproductive, I know.
However, the point is, a dedicated time for some code restructuring will often sound like a joke. Unless it's a side project on your own, good luck in convincing your manager. They'd have a concrete solid argument – numbers. Totally reasonable, who wants to pay for a prettier code, while software remains literally the same? As I said in the beginning, refactoring won't necessarily bring any billable value to a product. Performance wouldn't jump higher, bugs wouldn't disappear.
Good news is, you don't have to convince anyone, if refactoring is just a part of your daily routine. Like using terminal, commiting in git, reading emails, etc. Such activities are probably seamlessly integrated into your workflow as only a fraction, thus no one really worried about time spent on them. Similar to an artist preparing a canvas before painting, refactoring is about a software engineer preparing a code before editing – a natural part of the job.
However, none of your initially small refactorings should suddenly swell into a fortnight marathon with a broken codebase.
Continually functional software is essential, 'cause it gives full control over the process. You could stop refactoring basically at any moment and leave the building switch to an actual task.
Use the proper technique
I've learned that basically two components shape the controlled refactoring: small transformations and fast testing, performed after each of those changes.
Small step-by-step transformations help a lot with preventing a broken state. A mistake may occur reasonably rarely by changing only a few lines of code.
The steps should ideally be as small as renaming a variable or extracting a block of code into a function. Consider automated tools, like the "Rename Symbol" command in VS Code, as they're often less error prone than manual editing.
Such changes could have a little meaning on their own. But since every of them is a move towards a better code structure, you'll eventually end up with satisfying results. Think of it as a chess game – a coherent strategy leads to a win. Even though a single move could seem quite passive or even odd, they're powerful together.
And even if something goes wrong, it's easy to go a step back and try again. Hence be generous in committing those steps into a version control system (e. g. git). Those are checkpoints. Go with meaningful commit messages like "extract the validation logic into a function" instead of generic ones like "refactor". If something goes wrong, you can easily navigate between those commits. Something broke at the very beginning, but you've noticed it only after a dozen commits? Don't worry and try git bisect. Don't worry about messy git history either, squash it before merging into a main branch.
In spite of how precise changes are, you can't be completely sure based only on a static code analysis whether everything still works the same way. We're still humans, right? Ideally, assure the health of a codebase after each piece of change. That's why fast and easy-to-run testing is required. In the best case, there're automated tests executed against the part of a system you're currently working on. Lack of automated tests? Probably it's a good moment to add some. Otherwise try to find the path of least resistance to test your system manually.
That was a brief overview of the controlled refactoring technique. I just wanted to share essential concepts within my piece of advice without aiming at being a definition guide. If you'd like a deep dive, I recommend starting with Martin Fowler's "Refactoring: Improving the Design of Existing Code" book. I drew quite a bit of thoughts from it, applied to my daily work, which inspired me to write the text you're reading now.
Remember the scope
It's pretty common yet understandable to go too far with a refactoring. Using the technique we just talked about, it might be quite tempting to just follow the rhythm and forget about an actual goal.
In my workflow I usually try to evaluate by asking the following question. Did my code become good enough to start with the actual implementation?
There will always be room for transformations towards perfection. Doubtful, that it could be achieved in the end. Moreover no matter how close you were, software is usually constantly being changed within its lifecycle. Over time and through follow-up fixes there's a good chance for a shift from the perfect match to the "why would one choose that way!?" reaction. So, why would you waste too much of your precious time on it? You might want to consider the Pareto principle for good measure.
Don't get me wrong. I don't encourage you to rush into things without consideration for quality. Refactoring definitely worth itself and increases both quality and speed of development. Better aim for the good than for the best, though. Restructure your code with the future in mind, but mainly focus on the present. Consider edge cases, but don't try to cover unrealistic ones. Follow paradigms, but prefer readability. Keep your own coding style, but don't let it lead you.
When it's better not to refactor
At last, what could be better from the time-saving perspective, than just skipping the whole thing?
But as a decent engineer, you'd probably like a good reason to pass.
I consider the rule of thumb, that refactoring of a working code without aiming to utilize it is probably a waste of time.
Every so often for one or another reason you might stumble across a random smelly code. Although the code is unrelated to your current work, it could be easily very tempting to improve it. Especially when flaws are on the surface.
In that case it's better to get going. But, I must say, I'm usually having a bad feeling leaving such a codebase untouched. Sometimes the bad feeling wins. I jump in with the hope to introduce quick low-effort improvements here and there. Then I start to notice how far it goes away from actual work and regretfully revert my changes. Be smarter than me and avoid such traps. It'll probably save you quite a bit of time, drained by possible follow-up hot-fixing.
How to refactor without overtime and missed deadlines? Seamless integration into the daily working routine, healthy amount of discipline, pragmatic level of quality were the answers on my way so far.
Hopefully they'll help you to improve your development experience. At the end of the day it's what matters when building things. Remember, these workflows constantly evolve – it’s an ongoing process, don't struggle to perfect it overnight.
I'm always looking for better approaches, please don't hesitate to share yours in the comments. And to challenge mine, of course.
The credit for the cover photo goes to Naja Bertolt Jensen on Unsplash.
And the special credit goes to @josefine for making this text real in the first place.
Top comments (9)
The best way I've found - like you say - is to not treat refactoring as a separate task. It should be part of your normal process. I would also suggest not working to deadlines, or doing overtime - neither of these are conducive to good work and should be avoided
You should treat it as a separate task with it's own acceptance criteria and testing though, here's a great post about this advice:
softwareengineering.stackexchange....
Great article, I can totally relate. I think it's worth mentioning that when you practice TDD, refactoring is already part of your work flow (the 3rd step also known as the blue phase: martinfowler.com/bliki/TestDrivenD...)
Thanks, really appreciate it.
I haven't seen yet many engineers intensely practicing TDD, though. For many the technique is clearly quite extreme. But yeah, your addition is surely relevant.
Totally agree!
There is probably a small typo:
Should be „…less error prone…“?!
You're absolutely right, thanks! Corrected.
Not 100% happy with this argumentation, depending on lifetime of code and project. Just sell it to save working hours for all devs involved. A couple of scenarios that come to my mind are:
So for most code bases (at least the ones i came across in recent years), it's in my opinion good to start with some initial dedicated time and it's possible to sell this if necessary. If this is done and matches the code quality guidelines of the team (if present), it's fine refactoring on the go. As you implement new features, make improvements, ... .
Great article - thanks!
Oh, as a developer, I don't like this rationale either. Unluckily, I experienced it quite often so far. It doesn't mean by any chance a management's failure, though. Probably I wasn't looking quite convincing, 'cause find the argumentation still partly reasonable.
The scenarios you suggesting might work quite well. Especially if you're starting working on a stale codebase and sure about the upcoming area of activity. But still, why wouldn't you just keep those things in mind to dev team and serve it to the management under a sauce of a higher estimation due to poor code quality? From my naive point of view I would rather buy that, if I were a manager.
Thank you! I'm glad you like the article.
Nice article.
Takeaway: avoid such traps, quick low-effort improvements here and there.
Simply put
'Always leave the code better than you found it.' - Robert C. Martin (Uncle Bob)