Learning Legacy Technologies
Podcast: Play in new window | Download (56.2MB) | Embed
Subscribe: Apple Podcasts | Spotify | Email | RSS | More
Dealing with massive legacy code projects is not the same as dealing with brownfield projects written in more modern times. Not only is the approach to code itself likely to be different, but the way people handled errors and failure conditions is often substantially different. Further, unlike more recent brownfield code, it’s very likely that you can’t even locate the original developers. Even if you do, it may have been a decade or more since the last time they touched the code. It’s probably harder to build, harder to deploy, and harder to deal with in general compared to newer codebases. The code may have major security and stability issues that you can’t address without upgrading some components or even changing the way major parts of it work.
Adding to the fun, if a truly old codebase is still in use, that means that it is often central to the business purposes of your organization. Not only does this mean that there are major consequences for breaking the system, but it also can play havoc with your ability to make major changes to the code. Worse still, the people in charge probably remember that the previous developers could fix things more quickly than you are likely to manage. Finally, even if you do a wonderful job of fixing the legacy code and bringing it into the modern era, management will almost never acknowledge how difficult the task was, although they might now happily assign you to other legacy codebases.
Legacy code is tough to deal with. If you are used to modern development practices, getting stuffed into a legacy project can often feel like being plunged into icewater. However, it doesn’t have to be that way. With an ordered, structured approach, you can take control of a legacy project and put yourself into a better position, even if doesn’t mean learning the latest javascript framework. Best of all, such projects are often critical to keeping systems running and highly visible to the important people in the organization.
Episode Breakdown
Actually getting the code.
This can be a huge pain, as it’s entirely possible that it was never source controlled in the first place. You may legitimately have to find a 3.5″ floppy drive. Or worse. Verification that the code you have is the correct code may also be impossible. Some of the code assets may have hard-coded links to file system assets that don’t exist.
You’ll also need to hunt down any third party components and library projects that were in use the last time this code was worked on. This sounds easy, but many companies did a very poor job of managing dependencies a long time ago. For instance, there may have been a network share where dlls were dumped raw, and people copied what they needed out of there. Many legacy projects were old well before things like package managers were common.
You are going to have to find someone in your organization who can track this stuff down. While you can’t really fix this yourself, you can keep management in the loop as far as the things that are blocking you. The goal here isn’t necessarily to fix the problem. It’s to make sure management knows how big the problem is.
Getting it to build.
Once you have all the various bits of code you need on hand, now you have to find a compiler that will work. This can be tricky, as some older IDEs have difficulty on modern operating systems. You may need to run it on a VM. You will probably also learn that the specific version of the compiler will matter a lot – the previous developers may have had conditional compilation flags in place that only worked on certain versions of the compiler. Depending on the compiler they used, it’s also possible that you’ll find that there are additional build scripts that you need.
During this process, you are also going to find all the places where you screwed up on getting the code. You may find that the code you have is missing pieces that are required for the compile. You might also find that there are binary resources that are required for the build that you didn’t know about. You may even have corrupted files. Old source control systems were bad about barfing all over your filesystem with merge conflict files that your compiler may try to read anyway.
To make this easier, make sure that you keep the relevant people in the loop as to your progress. It can be tempting to isolate yourself and keep slamming your head into the wall to try to fix this, but that’s a really bad idea from an office politics standpoint. You wouldn’t be doing work on an ancient system unless it was critical – make sure that management knows you are working, what’s blocking you, and all the things you’ve tried. If you are in this mess, the person calling the shots is probably way up the chain from you – make sure your manager looks good.
DO NOT RUN THE CODE UNTIL YOU HAVE A BACKUP OF PRODUCTION!
People did a lot of stupid things back in the day, including working directly against production. You don’t want to cripple a running legacy system before you even have the ability to fix it. Check any configuration files that you have to make sure they aren’t pointed at production.
Making it run and getting to a stable state.
Your app may run on the first try after a build, but it is more likely that it will fail with some obscure error before it gets anywhere. Runtime environments have changed a lot over the years, and configuration-as-code basically didn’t exist years ago. In addition to runtime issues, the first run will start showing you where your configuration is incorrect. You may also have additional issues if your code is dependent on the operating system, as things sometimes change (for instance, the Windows API has changed a lot since the first time Will used it in 1995).
Runtime errors may be difficult to track down. Many times, the error will be something completely unhelpful, such as a numbered error code, access violation, or just a hard crash. Sometimes you’ll be lucky and there will be logging, either to log files or to an event log somewhere. Sometimes you’ll be especially unlucky and the logging system will be the cause of the error. You may also get runtime errors from dynamic loading of other components that weren’t specified in the build.
There are only a couple of ways to fix these issues. You either need to be able to pinpoint errors by logging or by debug breakpoints. If you got a numbered error, you might also look into the codebase to figure out what kind of error numbering scheme is used. This may help you find the source of the error. The underlying code framework may also have numbered error codes. Check on this as well. For instance, visual basic had lots of them.
Troubleshooting configuration
A running version of the application is only a starting point. Your configuration is probably wrong. You’re going to have to figure out where configuration information was stored. Depending on the complexity of the system, that information may not all be in the same place. Be especially cautious of things like server paths, username/password pairs, and the like, especially hard-coded ones, as you may need to refactor the code to put them in configuration just to get the thing working. Remember that whatever configuration was stored with the code may or may not be the configuration for the production system.
Establish a configuration that will work in your development environment. You need a parallel version of the system that is completely separate from production. Remember that you don’t know the system well and that you really need to avoid surprises during this phase of things, because you can’t effectively mitigate them.
Change as few things as possible while trying to enforce a clean separation between this code and what is in production. You may be pressed to fix bugs before you have a clean working version on your own machine. Resist the temptation to cave in on this, because you might break production while doing so. This is also a good time to spend some time going through the running code to make sure that your version really matches what is in production. If you see any discrepencies, you need to track down the reason why. This is also a good time to take screenshots and start making notes. Keep visuals of any relevant screens alongside any notes that you have – this makes it easier to make sense of the codebase.
It’s Data Time
Once you have a working version of the application, now it’s time to build up a development version of any data you are using. You may be tempted to simply copy production, but this can lead to a lot of problems. Production data may include paths, or may include sensitive data that is subject to compliance rules. Data of this sort can be a real liability when you are trying to work on the application. You can also easily make a mistake in production if the data is the same, not realizing that you were connected to the wrong system until it’s too late.
Management is unlikely to see the need for sanitizing data and getting a copy that is different than production. You should point out that the development version of the data may need to be modified heavily so that you can quickly test a lot of scenarios that may not be represented in the current production data. Compliance is another issue. If you did happen to get into contact with people who can help you outside your organization, issues are easier to troubleshoot when the data you are sending back and forth is fake. You should also insist on your own copy of any database server, file server, etc., used by the system. This lets you test serious system failure conditions without breaking things for the rest of your team.
Document the system as it is.
If you don’t already have documents for the system that are reasonable, you need to create them. You don’t necessarily need a lot of detail, but you do need to draw out (probably on paper) a rough system design so as to build familiarity with the overall system structure. Documenting the system as it appears in code is also necessary, because you can’t necessarily determine how accurate the existing system documentation is. Source code might have been under version control when it was written, but it’s less likely that documentation was managed with a similar level of discipline.
You shouldn’t start coding until you have a rough, but accurate, breakdown of how the pieces fit together. First, this makes sure that you aren’t making changes and seeing if you are even in the right part of the system. Second, this gives you some time to build a mental model of the system so that you can test assumptions later. It also creates a store of documentation that other team members can use if you manage to get help on this.
Now fix source control.
Once the system works, make sure you have it into a reasonable source control system. This means an appropriate strategy for branching and merging. This also means breaking down the major components of the application (as documented previously) into logical groupings where required. For instance, you may have some libraries that need to be set up in git as submodules, or may need their own separate projects and builds set up.
Getting proper source control set up allows you to rollback quickly if you break something. You are going to break a lot of stuff on legacy systems. Systems were not as clean back in the day as they are now. Being able to revert to a previous source controlled version is critical for being able to recover from screwups. Branching is also going to help you a ton, as you probably have a mix of bugs, which are liable to change priority while you are working.
Make sure and explain to management why you are doing this in terms they understand. You can’t count on your manager to understand the value of having proper source control around this stuff, so don’t bother. Instead, explain it in terms of being able to both extend the system and manage any bugs that come up during the process – the branching feature of many modern source control systems is an absolute godsend when dealing with the sort of managers who are constantly adjusting scope and priority.
Now you can actually work, but on what…?
Don’t pick your first bug based on priority, but rather on location. Pick issues that are around the edge of the system, or in places without many dependencies. Also be cautious about what parts of the system depend on the code you are touching, as you want to limit your risk.
Try to pick smaller issues, even if they are relatively minor and cosmetic. The point of initial bug fixes is not to fix the system, it’s to get comfortable with the system while showing some results. By the time you are at this point, management would probably like to see some progress, so this delivers that progress, even though it may not deliver exactly the progress they might want.
Continue to iterate here, taking larger, more difficult bugs that touch larger parts of the system. If you are working on a truly old system, you aren’t going to learn all the weird corners of it, even in a few months. This is a classic example of an explore/exploit problem. You should exploit (fix bugs) in an area until your learning slows down, and then you should explore (fix bugs in new areas) to continue your growth.
Now you should be able to fix the problems management hasn’t considered.
Management may not have considered security bugs, out-of-date components, and the like. With some familiarity in the system, you can now start to dig into these things. Security risks to the code are a major problem, but you aren’t equipped to fix them until you understand the codebase. You may also find that your hand gets forced on this – sometimes the security team finds major issues before you are ready.
Approach patching and updating the same way you previously approached other bugs. Try to start with things that have few dependencies and few dependents. You may find “interesting” issues in your build process or other systems while updating third party components, especially if the newer versions introduce incompatibilities.
If you are storing dependencies in the file system, now is also a good time to consider slowly migrating to a package management system. You’ll have to approach the move to a package management system by upgrading things at the lowest level first. Depending on how packages are structured, you may find yourself reworking code as packages are upgrading. A lot of older code may have been tangled together in a way that made it work poorly with package managers, and you’ll probably run into it.
Testing
Testing is also going to be a lot of “fun” in truly legacy code-bases. Depending on the platform, unit tests may not even be possible. You may be stuck writing integration tests. You should also start introducing seams, where possible, that allow you to write effective tests.
Testing is also going to require some changes to existing code. You may want to start making changes that allow you to use tools such as dependency injection to provide services to your tests. As you find code that is testable (or write it), try to introduce tests.
If management is hostile to the idea of automated tests, don’t tell them about it. Really, you’re going to need to be able to make sure you didn’t break things when you make a change, so you have to write code to catch major issues. If management has allowed an app to become extremely long in the tooth, they probably have a resistance to newer development techniques. Their attitude is the reason some of these apps stick around for longer than they should. Whatever you do, don’t set up a dynamic where management forces you to do things in a stupid way and then blames you when they get stupid results.
Some final notes
Legacy code is not necessarily bad. If it did the job for years, it’s actually pretty good. These projects often have a lot of exposure to higher ups in the company – do well and use the exposure to your advantage. If the system is going to continue to be used, being the main resource for it can help your job security and give you negotiating leverage. Most other developers will shy away from nasty legacy code. Play upon this to build your reputation.
Be willing to leverage information asymmetry with management. There are things they need to know and things they don’t. Legacy systems are a good place to talk your way into other parts of the organization and to make powerful allies. Old, poorly-structured and out-of-date code will teach you things you can’t learn elsewhere. You’ll see why things are the way they are now.
Book Club
How to Think Like a Coder (Without Even Trying!)
Jim Christian
Section three looks at learning language. It starts with a brief introduction to machine code then gets into what is syntax and how it can be used or misunderstood. It even goes into how IDEs help with syntax. Next the book talks about forms of coding discussing the differences in interpreted and compiled languages. The book specifically discusses object oriented languages before going into data structures. Then it dives into the core components of code: algorithms, loops, conditional logic, operators, etc. Finally the section closes out talking about debugging.
Tricks of the Trade
Mercilessly leverage things that are to your advantage. Stop seeing things as a disadvantage and figure out how to turn things to your favor. Captain Jack Sparrow your way out of it.