In this article, we’ll explore techniques to modernize a legacy Java application. For more insights, you might find Working Effectively with Legacy Code and Clean Code helpful reads.
As software engineers, much of our day-to-day work involves extending and maintaining existing codebases. While we may often feel frustrated with legacy code and the original implementers, complete rewrites are rarely a good solution. So, let’s discuss ways to make our everyday work a bit smoother.
Test Harnesses
The main issue in legacy code is that we often don't know what it is doing. And there is not good test code to ensure that after our change, the previously existing code is still working as intended. So before we can change any behaviour, we have to figure out what the current behaviour is. One way to do that is to write unit tests. By creating these tests, we figure out what is going on. And at the same we can be more confident in our change afterwards, since out test ensures theat the behavior stays the same.
Avoiding bad test code
While a extensive suite of unit tests allows for faster changes, we also need to make sure that we test the right thigs. Nothing is more frustrating than a Unit Test suite that breaks at every internal change we make in our components. Therefore we need to insure that we test at the right level. We need to figure out what parts of our software need to be tested together and what needs to be mocked. This is no easy task and requires a lot of experience. If you test your mocks out all dependencies, it is probably to small. If you need to start up your whole application for one test it is probably to big. It is important that you have the same coding standards for your unit tests as you have for your production code.
Exploring code by mechanical refactoring
Another approach to figuring out what a pice of code is doing is to try to refactor it. Since java is a well defined language (this doesn't work in python or JS), there is a range mechanical refactorings we can apply to our code that have low risk of breaking the behaviour of our system. By using a IDE like Intellij, all of that is automated and can be quickly performed and reverted again. These refactorings includes: extract method, inline method, create method object, ...
Scratch refactoring
Scratch refactoring is a technique where we don't intend to keep the changes we do to the code. The only outcome we expect from this exercise is to get to know the code base on a deeper level. Since we don't keep the changes we make, we don't have to be as careful with our changes.
Method Object
If you have big methods with lots of temporary variables, which are very entangled and you have a hard time disentangling them, you can create a method object. In the new class, the old temporary variables become fields of the class. This allows you to more easily see dependencies in the previous methos. It also allows you to separate the complicated logic into its own class, making the previous class easier to understand. Als with any refactoring this adds a certain level of additional abstraction, so be careful that it actually provides value.
Boy scout rule
The most important rule for working with code applies especially for legacy code. Always leave the code more clean than you found it. This allows your codebase to improve over time, making it easier for other engineers (or future you) to make changes.
Remove Automagic
As developers we tend want to make our jobs easier. That`s why we love to add abstractions and automatons to our code, that help us avoid tedious work. This code works like magic. It just does the boring thing in the background, while we can go on doing cool and interesting work.
However, this sometimes creates huge pains for the developers that have to maintain our code. Since it works like magic, there is no obvious connection between what is written in the code and the behavior of our systems. Examples of automagic often seen in the wild are annotation processors, Reflections and AspectJ. While these things are incredibly useful and can greatly increase the conciseness of your code, if used incorrectly, they can cause significant headaches for future maintainers.
Break out modules
The most important and at the same time hardest part of working with any code base is to create independent modules. While this leads to duplicate code and additional work, it is often worth it. After the change, you can evolve your modules separately without worrying that changing one part of the system leads to broken behaviour in another part. After the separation, two separate teams can work on the modules without stepping on each others toes.
Are you struggling with a legacy application?
We can support you extending and modernizing your application landscape.