How not to go crazy when working with Legacy code
Most developers sooner or later will end up with a project that will contain legacy code. It is worth to be properly prepared for such a meeting, because each of them is a unique experience that guarantees a large dose of stress, feelings of helplessness and irritation. So how to approach the topic so as not to go crazy and achieve the desired goal?
Legacy code-what is it eaten with?
We should start by approximating the definition of legacy code. This will be crucial for understanding the issues described. The Polish translation states that this is an “inherited code”.
However, this name is not entirely appropriate. It originally meant a code that is dependent on unsupported version of the system, language, framework or other software. It is maintained because it still works and gives the expected results.
The concept of legacy code has evolved over time in the development community, so this definition, while correct, may not be sufficient.
This topic is taken up by Michael Feathers in a book entitled “working effectively with legacy code”. It uses the term legacy code, understood as “code without tests”. So you can create an application from scratch, which already on the day of release will be a monument.
However, this is not the main thing here. The reason why feathers ‘ tests are so important is to see if the system will still work as expected after the change. I will only add that in my opinion poorly written tests are just as, and sometimes even more dangerous than their absence.
What can be the reasons that the developer did not write the tests or did it just wrong? Maybe he didn’t know how to handle it. Another factor could be, for example, “lack of time”. In the first case, this alone guarantees very poor code quality, and in the second case it may indicate problems with the application architecture or project management.
Poor quality code is very difficult to test. Usually, when we look at legacy code, it is more or less spaghetti developed without much thought about how to maintain the system in the long term.
In summary, Legacy Code is characterized by lack or poor test coverage, which deepens the mess in the code and architecture. If we add to this the violation of good practices, often perpetuated by “generations” of developers, we get a not very pleasant picture. With all this we have to deal with-the innocent of the whole situation.
Is it really that bad?
Of course, there is nothing to exaggerate. In the worst case, we will get to spaghetti without tests, in which any change can bring completely unexpected problems, and this is really annoying.
In fact, we rarely encounter such a situation. Systems of this kind still persist, because they work all the time and probably earn money (some of which goes to your salary).
Represent real business valuebut in order for the money to match, it is necessary once in a while to make a general tidy up in the system. This can be refactoring or stabilizing the system. After all, no business can afford to let its product stop working completely.
As it turns out, rewrite the system again, in 98% of cases is not a good solution, because it will generate huge costs, and there is no guarantee that it will be better than the previous one-both in terms of business, as well as the cost of living.
You can also list more pluses of working on legacy systems, as Robert pankowiecki wrote on the Arkency blog (I recommend reading).
Imagine that you sit down to work on this type of system and you have to add a new feature. You realize that when you do, the only way to check if it works is to manually click through the new feature.
Unless you change the code in a few other places … by the way, you analyze the code more and more deeply, and it turns out that half the system is tangled with each other, so probably a dozen percent of the existing tests will need to be fixed after the feature is completed. you need to decide what to do.
Bad choices when working with Legacy code
When working with legacy code, two errors are most common.
Rewriting your entire code from scratch
Such tactics require in-depth analysis and detailed knowledge of the system, and the cost of acquiring this knowledge can be very high. It takes quite a long time and can cost a lot of stress.
When a large change is made hot, it is very likely that the system will stop working properly. Earlier generations of developers worked to solve specific problems that you may not even know about. Time has shown that these solutions have worked-even if they are misspelled.
This includes the issue of technological debt. A step change generates additional costs associated with a longer period of stabilization and repair of other functions that are affected unintentionally by the change. Usually the project manager does not agree to this, and for us this situation will not be very pleasant.
I will do it as others have done before me.
If code quality is important to you, it will rarely be a satisfactory solution. From the point of view of the system, this means further taking on technological debt. It should be borne in mind that interest is paid on this debt. At some point, this can paralyze software development.
A good example here is my own professional experience. I worked for a client who inherited a big system in 2012. It worked, but it wasn’t well written.
Since then, the new owner of the code has written a lot of code, following the old style. My task was to upgrade the most outdated solutions, but I had the opportunity to observe the work of the team working on the development of this software. What were the consequences of continuing the old policy?
The release of subsequent versions was delayed from 2 weeks to more than a month (with bimonthly releases-sic!). The moment of entering the production was equivalent to extinguishing a fire, tickets were closed and opened alternately several times, due to problems found later (as of the end of 2015).
The good news is that they are slowly coming to terms (as of December 2017) because they have broken with some of the bad habits 🙂
Which way is right?
The key when working with such code is attitude. Repeating to yourself that this code is weak will not change anythingand it will only lower morale. So what should be done?
First – concentrate on the task that you have to perform. If there are problems, consider whether you can really solve them in a meaningful way. Ask other developers what this solution entails.
They will often be able to tell you something about the history of this piece of code. Pro-active attitude is the basis, but be prepared that not every idea you will be able to implement.
My observation is that in most cases it is best to make small improvements. This is that if I’m working on a piece of code, every time I try to leave it in a better state than I found it.
An example of an improvement is the addition of tests for untested cases that may actually occur. Local refactoring of the class so that it would be more consistent with good practices (but it does not have to be perfect right away!), or the introduction of a missing abstraction (and even better the removal of one that is unnecessary).
These small changes are acceptable where I see and understand the dependencies with other parts of the system. As a rule, I don’t modify what I don’t understand yet.
It is important to cover these changes with tests, so that future “generations” will have easier tasks of this kind. These improvements add up-often another developer will adopt a similar solution to solve the same problem. Over time, this can turn into a clear change.
The fact that it was bottom-up-that is, the smaller components were changed first-translates later into a lower cost of further improvements at higher levels.
What I write about is not always easy, sometimes it can be downright impossible. You have to have an open mind and consider different solutions. It is also worth reading the aforementioned book by Michael Feathers, in which specific techniques for dealing with even very severe cases are described in detail.
Legacy code is not the end of the world, so there’s nothing to stress about. It is worth doing your own and draw conclusions about what you should not do. Yes, it sounds vague and it is in reality. This is the general approach to the problem. With a little luck and intelligent work, after a few months we will see the effects of our changes.
I used to work on a project in which a group of “volunteers” had to be available for each release to extinguish a potential fire, but when I left the new version and its upload to the client was no longer unusual. This was not the result of large refactoring and constant rewriting of individual services.
This the sum of small changes inside the code and getting better testing were crucial here.