So I’m here to talk about the legacy you don’t want to leave. Unfortunately, this is a situation that a lot of developers have to deal with, but it’s not something that’s talked about too often. Legacy Software is something that most of us are going to have to deal with at some point. If you aren’t part of that group, consider yourself lucky. For the unlucky ones I’m here to share some advice with you that I’ve learned the hard way. Hopefully I can help you get on the right track.
This article is going to deal with these topics at a high level, but if there is something you’d like to dig into in more detail leave a comment and let me know. I’d be happy to discuss it in a follow-up post.
First, There’s Hope
Listening to everyone go on and on about DevOps, Continuous Integration, and 90% test coverage can be a bit disheartening when you are staring at hundreds of thousands of lines of spaghetti code with so many interdependencies you don’t know which module to compile first. But, there is hope.
Let Go Of Perfect
It will help to let go of your ideal. I’m sure you have some glamorous idea in the back of your mind for rewriting this legacy in the hottest new framework and having 100% test coverage but chances are that’s not going to happen. You need to let go of your idea of perfect and realize that all you need to strive for is just doing a little bit better each day.
Once you understand that you are just trying to do a little bit better each day, the process becomes much less overwhelming.
All New Code Is Under Test
One of the easiest changes you can make to improve is to follow new best practices for new code. Just because the existing code base wasn’t written in a way that allowed for easy unit testing doesn’t mean new code has to be written the same way. So when adding new code:
- Isolate new code, ideally in new modules/packages/libraries altogether.
- New code is completely testable and tested.
- In old code, change as little as possible, call out to knew tested, trusted code.
Obviously this makes the most sense when adding new features. Fixing bugs and changing functionality gets trickier but we will talk about refactoring soon.
One hurdle I ran into initially was finding testing frameworks. The first hurdle was the version of the compiler we were on was not supported by some of the popular testing frameworks. We ended up upgrading our compilers. If you are thinking to yourself that you can’t afford to do that, I suggest you go look at the bug report for that legacy compiler and ask yourself if you can afford to stay on it. The second hurdle we ran into was that there was no reasonable unit testing framework existing for the technology in use. In our case we wrote our own. It was nothing glamorous, and not the most full featured but it did the job and it fit the mold of just trying to do a little bit better.
Build Pipelines Aren’t Just for DevOps
In order for your tests to be worthwhile, they need to get run. You should be writing new unit tests and acceptance tests and these should be run every time you change the code. In order to ensure that this happens you need to automate your build pipeline.
Scripts Aren’t Enough
You’re probably already using something like make and even have scripts to run your entire build, but unless this is happening automatically, and kicking off your tests and reporting results, you can do better. You need your builds kicked off automatically every time the source is changed in SCM. Hopefully I don’t even need to mention SCM. If you aren’t using git, it’s time to change.
Jenkins To The Rescue (Mostly)
Our solution was to use Jenkins. We supported some pretty old versions of AIX, SunOS, and HP-UX so getting java installed was a challenge. Some of these instances wouldn’t support java versions newer than 1.4.
One solution was to run our legacy builds on an older version of Jenkins that supported slaves using an older version of Java. Although not ideal it’s working for us and again, was better than what we had before.
For the oldest platforms that we couldn’t get working with java we built and installed SSH. This allowed us to kick off our build and test scripts from Jenkins using SSH. Although SSH works, using the legacy Jenkins provided more feedback as it utilized true Jenkins slaves.
The Boy Scout Rule
There is a rule in scouting that you should always leave the campground in a better state than you found it. Developers should adhere to this rule as well, and it certainly comes into play with legacy code. Legacy code does offer a unique challenge, though. If you don’t have high test coverage, how do you have the confidence to refactor?
It’s circular logic I know but you need to refactor enough to have good test coverage and you need to have good test coverage to refactor. This is where things get slow and tedious. You need to be very careful about understanding the intent of the code you are refactoring. Ensure you create tests that cover the functionality you are refactoring and you can be confident about the code you changed.
The boy scout rule also applies to code coverage. If you can, find a tool that allows you to track code coverage and then reject pull requests into your SCM that don’t improve that coverage. Rational Purify has a coverage tool that works relatively well for C and C++. There are a number of options for Java.
I know that’s a lot to cover but the summary is pretty straight forward, just do a little bit better each day and eventually you’ll get there. If you don’t whittle away at that technical debt eventually it will grind your progress to a halt. Keep an eye out because in the future I’m going to dig into some specific legacy anti-patterns and show you how to improve them.