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.
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.
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 more 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.
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.
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.
Source: DEV Community