how-we-work-efficiently-with-legacy-code
Engineering
Jan 30, 2020

How We Work Efficiently with Legacy Code

GetYourGuide
Careers Team

Senior backend engineers, Ondrej Führer and Sergei Kukhariev offer strategies on how to deal with legacy code. Working with it requires its own set of skills, and below, we're going to give an overview of them.

{{Divider}}

Introduction to legacy code

As software engineers, we're continually learning throughout our careers. New tech is always around the corner, yet good software habits require practice. Learning materials in our field are mostly taught based on greenfield projects where you start from scratch and learn something new as you go.

While greenfield projects are incredible for learning, the reality of real work is quite different. We're always working on top of someone else's existing work. We're often tied up with business constraints or simply don't have enough experience with the language, project, or tool to make a fair judgment of which approach is right or not. Because of these discrepancies, many engineers look for greenfield projects where they can apply their skills and write code faster.

But, the grass isn't always greener — when you limit yourself to new projects, you lose an opportunity to learn from the mistakes of others. Think about it. Regularly working on greenfields doesn't give you any feedback.

You simply move on and never learn what from you may have done wrong.  Legacy code, on the other hand, gives you opportunities to learn, grow your empathy and reasoning about the code. Sure, it could be frustrating at times, as frustrating as it was to write the first few lines of code.

We’ll go through this topic in 10 digestible sections. Feel free to click on the topic of interest to go directly to that part or scroll down, to immerse yourself in the full reading experience.

  1. What is legacy code and what problems does it create?
  2. What is code refactoring?
  3. How working with legacy code builds empathy
  4. What are characterization tests?
  5. What is the process of refactoring a class with characterization?
  6. What are dependency breaking techniques?
  7. When your class receives untestable/unmockable dependencies
  8. What to do when you can't construct the class you want to test
  9. What to do when testing a private method
  10. How to: Break Out Method Object

1. What is legacy code and what problems does it create?

There are many different definitions and interpretations. So we'll work with Michael Feathers' common definition: Code without tests or Source code inherited from an older version of the software. In both cases, which are usually combined, we have a few challenges.

If we look at legacy code as an old code without tests, we can look at it as a black box — it's code that works and was running on the website for years, yet we have no idea why or how. Over time, the complexity of a codebase only grows.  

Adding new functionality is dangerous, and refactoring is nearly impossible. That leads to increasing development time needed to make any changes or making the changes infeasible to do. If we need to change the code due to a long-lasting bug or an edge case, we can't be sure we didn't break anything else.

Other issues could be business constraints at the time the code was written. Nowadays, no one remembers why, and the people defining those conditions are long gone, so we can't ask them.  

2. What is code refactoring?

Martin Fowler introduced refactoring in his book, Refactoring: Improving the Design of Existing Code. The meaning of code refactoring is:  

Refactoring (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.  

By definition, refactoring is not possible without the safety net of tests, as you can never be confident you didn't change the behavior or break something.

As we described legacy code as code without tests, we can see the problem here — we cannot update the code to our latest standards without fear of breaking or changing the current functionality.

3. How working with legacy code builds empathy

There is a famous joke in our community:    

how we deal effectively with legacy code.jpg
Most backend engineers can relate

There's an element of truth to every joke, and without a doubt, most of us have had similar thoughts about the code we worked with. In reality, our predecessors never intended to write bad code. It's our judgment that tells us that this code is terrible, but to the person writing it at that time, it seemed to be the best option.  

Putting yourself into the original writer's shoes and doing a bit of research to answer these questions can help when trying to understand legacy code.

Working with legacy code requires a certain level of empathy. Without it, you're bound to remain frustrated.

4. What are characterization tests?

As mentioned above, legacy code is often defined as code without tests. Adding tests to legacy code offers many benefits. As you write more and more tests for a legacy class, your understanding of the code builds up, and it becomes much easier to change.

But anyone who has ever tried to write proper tests for legacy code gets frustrated quickly — it seems next to impossible because of all the coupling, static calls, global dependencies, unstructured code, and so on.  

While proper unit tests capture and document the small nuances of the code behavior, that's not exactly what we need at this stage. We need a safety net that can help improve the class design. Characterization tests are a tool for that. These are the tests that capture the current behavior of the system, without being worried about the correctness of it.  

The main quality of characterization tests is that they are throwaway tests. Their goal is to help you reason about the code and refactor it to a more testable state, not to stay there forever.

That gives you a lot of flexibility on how to write them. You no longer need to mock things. You can run them against your database. The speed of these tests doesn't matter that much, and you also don't need to run them on your CI server.

5. The process of refactoring a class with characterization

1. Write as many characterization tests for the subsystem you’re about to refactor as you feel necessary.

2. Refactor one class at a time, using dependency breaking techniques, until those classes are testable.

3. Write proper unit tests until you're happy.

4. Delete your characterization tests.

5. Commit your code.

6. Dependency breaking techniques

Over the years, a few patterns of working with legacy code have emerged. Most of them are about decoupling code.

Mastering them will give you benefits outside of the legacy code world — you will be much better at knowing how coupling works and develop a good intuition on structuring your code.

Most of these techniques are very well documented in a brilliant book, Working Effectively with Legacy Code, by Michael Feathers. We recommend this book as much as we do Refactoring by Martin Fowler.  

7. When your class receives untestable/unmockable dependencies

Pass Null: Just pass null instead of the dependencies and see what happens when you run the test. In many cases, you will be able to run some of the unit tests for this class. And some is infinitely bigger than zero.

Extract Interface: Add a new interface to your dependency and use the interface in the class you're writing tests for. This will allow you to mock or stub your dependency.

Use Mocking Library: Mocking libraries are quite powerful these days, they know how to mock not only regular classes but abstracts, static methods, and so on. Invest some time into learning what your mocking tools can do for you.

Be careful about this pattern –– too many mocks make your tests hard to maintain; they become fragile and tend to break in batches.

Subclass and override: In this technique, you would create a subclass of your main class, and by overriding some of the nasty behaviors, you'd make this subclass testable. This is very useful when you want to refactor just a small part of the parent class that is easy to isolate.

8. When you can't construct the class you want to test

Parameterized Constructor: The usual starting point, bread, and butter of working with legacy code. Try to inject some of the dependencies of your class that give you trouble while writing tests.  

Extract Factory Method: This is useful when your class has static dependencies that, for example, grab configuration. The idea is that you move those static dependencies to a factory method, while the rest of the class stays decoupled from those static calls.  

Make Constructor Public: Singleton, one of the original patterns from the book, Gang of Four, by Erich Gamma, John Vlissides, Richard Helm, and, Ralph Johnson, over the years proved to be an antipattern in the majority of cases.

Its usage is mostly eradicated, but you can stumble upon it every now and then. The solution is straightforward — make Singletons constructor public and use it to write tests.

Set up and tear down: Most of the modern testing frameworks provide this functionality. Use it in your tests when you have to deal with a global state you have control over.

9. What to do when testing a private method

Maybe you shouldn't — testing private methods breaks the encapsulation of the class. If the private method has a lot of logic, then you should suspect a hidden class.

Break Out Method Object refactoring: Another bread and butter technique while working with legacy code.

10. How to: Break Out Method Object

1. Create a new class

2. Create constructor and use the same signature as the method you are extracting

3. For each argument, declare and assign instance variable

4. Create an empty execution method.

5. Copy the body of the old method

6. Get rid of usages of the old class

7. Write tests

8. Replace the original method with a call to a new class

What next?  

The best thing every engineer should do after reading an article like this is to get a coffee or tea, whatever drives you, and start doing it. Practice makes perfect. There are many techniques you can use to deal with legacy code, but there is one piece of advice that we would like to emphasize here: Don't be afraid of legacy code.

Other articles from this series
No items found.

Featured roles

Marketing Executive
Berlin
Full-time / Permanent
Marketing Executive
Berlin
Full-time / Permanent
Marketing Executive
Berlin
Full-time / Permanent

Join the journey.

Our 800+ strong team is changing the way millions experience the world, and you can help.

Keep up to date with the latest news

Oops! Something went wrong while submitting the form.