Don’t Let Legacy Code Make You Suffer. Make It Suffer
Feeling like the codebase you’re working on is poorly designed? Wish you could focus on writing good code, rather than trudging through mud code all day long? Would life be easier if only the legacy codebase had a clearer structure?
If you answered Yes to any of those questions, be aware that you’re not alone. Quite the opposite, in fact. You only need to talk to people for more than a minute at meetups or conferences to realize that a significant amount of developers suffer from legacy code.
It makes me sad when I see competent and motivated developers losing faith and ending up suffering from the questionable quality of the code they’re working on. Some resign themselves to it and even spend years suffering from legacy code on a daily basis!
It doesn’t have to be that way. One of the ways to get out of that spiral is not to let yourself be bullied by bad code.
Instead, show bad code who the boss is.
Legacy code is a bully
As a young developer, starting to work in a codebase that has been there for a while can be a challenge. Even more so when you just graduated from CS school where you’ve mostly worked on libraries or ad-hoc projects. Being thrown all of a sudden into a big codebase that has evolved over years can be disorienting, to say the least.
It’s like you’re the new kid at school, and the big bullies don’t plan on making your life easier.
Big functions, big objects, mysterious names, inconsistent and duplicate components, all those bullies firmly oppose themselves to your understanding of the code. They’ll do all that is in their power to slow you down in your analyses, and even when you make a fix and think you’re done, they will throw an unexpected regression into your face.
But kids get bigger, bullies end up going out from school, and some kids even grow into the new bullies that will take care of the new generation.
This is where the metaphor breaks down. Even if you can grow as a developer, time doesn’t make legacy code go anywhere. It’s waiting for you, day in and day out, trying to get in your way. Some people spend years suffering from it!
If you’re in that case, I want you to take action by fixing the code. But not any action. I want you to come up with a targeted strategy, that aims at making your legacy code less powerful in its ability to make your life miserable.
Hit it where it hurts
There are so many things you could fix in a legacy codebase, so many places that would deserve a little makeover, or even a total refurbishing.
But you have to face the hard truth: you won’t fix be able to fix it all.
Codebases that took years of active work that involved several to many people are vast. Fixing every last issue would take months or years, and you have client requests to satisfy at the same time. Going off to a crusade trying to fix everything that’s wrong in the codebase is an utopia. Similarly, throwing it all to the bin and rewriting it from scratch is often a terrible idea.
However much time your company policy allows for refactoring, you have limited resources. So you need to pick your battles very carefully.
How to evaluate which refactoring is worth your team’s time? It’s comes down to a basic economic decision: you want to maximize the value and minimize the costs. But what are the value and costs of such refactorings?
The costs of a refactoring
The costs of a refactoring include the time to actually change the code, but not only.
Changing obscure code requires you to understand it first. So if you’re not clear about it already, you need to factor that analysis time in. Also, such changes could cause regressions, so you need to factor in the time you think it will take to stabilize your refactoring.
A refactoring that introduces boundaries and interfaces may give you an opportunity to write some unit tests around it, which may take some time too.
Also, it you’re tackling a buggy part of the code, chances are that somebody else in the team is currently trying to fix a bug in that same code, and integrating both your fixes will need to solve a merge conflict.
The value of a refactoring
Here we’re talking about diminishing the capacity of the codebase to get in your way. So it has to be code that you read – or debug – frequently. There is little point in refactoring code that you don’t interact with often. Even if you see how to make it better, and even if you feel it wouldn’t be too hard.
This brings up a very important point: why do we strive for good code? For art, because it’s beautiful? For morality, because it’s wrong to write bad code?
No. We write good code because it helps the business. Good code leads to less bugs, faster integration of new features, less turnover in the company. All those are business reasons. Refactoring a piece of code that doesn’t pose a problem to the business is tantamount to refactoring the codebase of another company while we’re at it.
Well, in fact there is another reason to improve code quality: it makes our lives easier, as developers. Sure, this in the interest of business too, but we can see it a goal in itself too. Anyway, refactoring a piece of code that doesn’t hinder us too much is wasted effort in that respect too.
In particular, and I know it may sound surprising at first, don’t do a refactoring just because it’s cheap. If it doesn’t bring enough value, you will have wasted time. You’ll be more grateful to have spent an afternoon making one big hit to a targeted part of the code, rather than 100 little flicks all over the place.
The most efficient approach in my opinion is to be value-driven: pick the 2 or 3 things in your code that slow you down the most or are the most buggy, and that have a reasonable cost of fixing. Conversely, don’t be cost-driven: don’t pick the cheapest fixes you could do and see which one is the most helpful.
Let’s now see what sort of hits could have a reasonable value/cost ratio.
Where does it hurt?
Before giving some suggestions, remember that you’re the one in the best position to figure out your most valuable refactorings. What annoys you the most in your codebase in a daily basis?
Also, you can survey your team to ask their opinion on that question, and decide together on what to take action on.
Here are some suggestions, and you’re welcome to suggest other ideas based on your experience:
Slice up a big function
This is a classic one. Big functions drown readers of the code into low-level details and prevent them to have a big picture of what the function is doing.
Identifying the responsibilities of that function allows to split it in several sub-functions and put explicit names on them, or outsource part of its work to another function or another object.
If you come across that function often, this refactoring can bring a lot of value.
Slice up a big object
Some objects get extra responsibilities tacked on one by one over time, and evolve into massive behemoths that sit in the middle of the codebase.
Splitting their members allows to manipulate lighter structures that take up less mental space in the mind of a reader.
Sometimes, slicing up a big function leads to slicing up a big object, if the various sub-functions operate on various, but distinct, parts of the object.
Expose side effects
Big functions making side effects on big objects are notoriously hard to follow. Making it clear what effects a function has on objects helps following along and being less surprised when debugging code.
One way to do this is to make more objects and methods const
, and separate the data that is modified from the data that is const
in an interface.
Having no side effects is even better but, as a first step on a large function, this is not always realistic to aim for.
Use names that makes sense
Bad names can send you on a wrong track, and make you waste a lot of time.
The value of changing some names can be high, and its cost varies from low for a local name to higher if the codebase uses the name broadly and you don’t have appropriate tooling.
What else would you include as refactorings with high value and reasonable cost?
In any case, don’t let yourself be bullied by legacy or otherwise bad code. Talk with your team, identify the painful points and how to fix them at a reasonable cost. Start small, with a couple of bad functions or objects.
And when you’ve identified you targets, hit them, and hit them hard.
Related articles:
Don't want to miss out ? Follow:   Share this post!