Legacy code. It’s something the “other developers” left behind. You know those guys. They were unprofessional. They were lazy. They didn’t know how to write good code. Or, if we’re being generous, we might say they were rushed for time. They had to get the product to market fast, so they cut corners.
The very term “Legacy Code” encourages this view. The code is a Legacy. It’s something we inherited. It’s our responsibility, but it’s not our fault. This paradigm gives us a light where we can shine blame — and a reason we can leverage to do a rewrite!!
But legacy code isn’t only something we inherit from other developers. It’s also the code we saddle ourselves with. I can’t count the times I’ve looked back at code I’ve written and thought: “What the hell was I thinking?” (usually followed by a refactor of said code).
If we’re honest, this is a more accurate definition of Legacy Code:
Legacy code is anything we’re not writing right now.
That legacy code you hate is paying your salary. Those stupid developers who hacked it together did a good enough job that you get a cool office, a new Mac Powerbook, and free Club Mate. Were they lazy? Were they unprofessional or not as senior as you are? Maybe. Maybe not. But, they deserve a little respect.
I’ve rarely met an engineer who didn’t care about their code. Until proven guilty, assume everybody’s trying to do a good job. Assume that everyone put their best effort forth.
This doesn’t mean that we can’t get frustrated by legacy code. This doesn’t mean that we can’t say we know more than the developers who came before us. But having a respectful attitude about the code base we inherited, the coders who developed it, and the problems it was trying to solve can transform our relationship with the code, and with the organization that created it. And, if that legacy code was written by the devs on the team you work on, having empathy for them, instead of contempt, will make the difference between helping that team turn around and creating a scared team of embarrassed developers who try to hide their mistakes from you.
Listen to the Legacy Code
Teams produce software systems that mimic the structures of their organizations — Paraphrase of Conway’s Law
We disdain and discard legacy code at our peril. Without paying attention to it, we have no chance to learn from it. When we get caught up in the “right” way of doing things, it becomes easy to forget:
- The quality of our code is a symptom of our organization. It’s not just the developers.
- “Right” is often a matter of style or preference —and can have nothing to do with whether the code is good.
- We’re always learning, and evolving — and so are our teammates. That’s why the code we wrote one month ago often looks horrible to us.
- The code is the way it is because people were trying to solve a particular problem. Learning about this problem — which may not be technical — can provide important insights.
- Our product is always changing.
- Legacy code is inevitable, because… (see bullet below)
- Ultimately, all code is legacy code.
What Makes Good Code?
In the long run every program becomes rococo — then rubble — Alan Perlis
As I said above, I believe almost every engineer wants to do a good job. They want to follow “best practices” and write good code. Driven by stories of the perfect code base that is bug free, thoroughly tested, easily deploys into production, and has the esteem of its peers, we look to build a Utopia. For what it’s worth, “Utopia” literally means “nowhere”.
Software systems break down over time. There are many, many reasons for this. They’re interesting, and understanding them can be important. But, what is more important is accepting the inevitability of imperfection and decline.
Why is this helpful? It moves us to focus on the important things. And these almost invariably turn out to have nothing to do with software.
Most of us write software for Organizations: Corporations, Small-to-mid-size Businesses, Governments, Non-profits, &c. Our software is supposed to solve problems. If our software solves the problem it’s supposed to solve, it’s good software.
If our software solves the problem it’s supposed to solve, it’s good software.
Or, should I say good enough? To be completely fair, that’s an insufficient definition — but it’s not designed to be perfect. It’s trying to make a point:
- It doesn’t have to be perfect. It never will be perfect. The most important thing is that it works.
- It has to be maintainable, and able to evolve. It doesn’t have to be totally easy to maintain.
- It doesn’t have to always follow “best” practices — which change over time, anyway, and are easily scraped as “Legacy Code” at a later date (CORBA anyone?)
- It doesn’t have to use the latest, or coolest tech.
The Power of Honesty
There are code bases we hate, and those that are literally unworkable. However, if your software is functioning and has an active user base, there’s a good chance your software is good enough to refactor. If the software was unworkable, your business would be noticeably suffering.
- You’d be losing customers.
- You’d be trailing the competition.
- Adding new features would be nearly impossible — either because you couldn’t figure out how the code worked, or touching anything could cause the whole edifice to collapse.
- Bug reports would come in on a daily basis, bugs would stick around for months unsolved, and fixing one bug would predictably lead to two or three new bugs being created.
I’ve encountered systems like these. They bear all the signs of the Titanic post iceberg: It’s pretty clear they’re going down. Then, there’s systems that are just frustrating. They’re a pain to work in. The code is ugly, unclear, poorly documented. It’s here where we need to be careful.
When we have to live with code that:
- Is hard to change
- Is buggy
- Is slower than we’d like
It can be tempting to rewrite the whole thing from scratch. If nothing else, pause before you make that decision, and at least take the time to really look into the scope of what you’re doing.
The Power of Acceptance
It wasn’t until I accepted myself, just as I was, that I was free to change — Carl Rogers
Importantly, this is NOT a call to laziness. It’s often said that good developers are lazy developers. I get what’s intended here, but I think it’s just not true. Good developers care deeply about their code. They are thorough, and thoughtful. They work hard.
This is also NOT a call to mediocrity. Rather, this is a call to reality. Systems are complex. They’re “messy” by nature. Much of a coder’s job is akin to herding cats. It’s trying to reason about complex relationships between multiple moving parts and information that changes over time. It is about working inside a complex human system — one that has a history, a future, competing interests, and ingrained ways of doing things.
This all means that our job is difficult. Because systems are complex, it will always be hard. It will always be easy to make mistakes. There’s always something new to learn. Because of the transient nature of our profession, there will often be multiple ways of doing things scattered across a code base — many of which we don’t like. There will be code written by people more senior than us that we don’t understand, and code written by juniors who made predictable mistakes. In a profession riddled with bad documentation — when it exists at all — there will be important decisions documented by cryptic lines of code.
Rather than throwing out the “baby with the bathwater”, and losing a lot in the process. Rather than getting angry and frustrated with the crap code we have to deal with. Rather than recreating the behemoth we swore to destroy. We can accept that our apps will never be perfect. We can accept the imperfections we have to live with now. We can take the time we should to learn from the legacy code we live with.
Acceptance and patience are antidotes to reactive thinking. They help us create the space we need to think clearly. When we have this, we can more wisely proceed to actually evolve a code base we can be proud of.
- Learn first. If you’re frustrated with your current code base, don’t rush into a rewrite. The code may be complex. It might be slow. It might be hard to maintain or evolve. But, there could be good reasons for this. Learn them.
- Refactor over Rewrite. Refactoring is considerably cheaper than rewriting. If you have working software, it’s a LOT less risky, as well. You make smaller changes, with less impact, and have more ability to easily pivot if your newer better code has problems.
- Learn to be at peace with imperfection. This helps you avoid making reactive decisions that could cost a lot in the long run.