At a recent CTO meetup in Edinburgh’s CodeBase we covered the topic of legacy code. Given that this is a common topic and that we had some of Edinburgh’s brightest CTOs weigh in on the topic I thought it would be good to summarise the main discussion points.
What is legacy code?
We started with a discussion about how you would define legacy code. It’s become something of a buzzword alongside technical debt – but how do you recognise it?
- Hard to maintain – no one knows how it was put together and are afraid of making changes. It becomes that curiosity that gets left because it works well enough and making changes seems too risky.
- It could have been done better – as technologies evolve and our teams continue learning it will often be the case that we look at an area of our systems and realise it could have been done better. This does not mean it was done poorly the first time – just that it was a while ago and we want to always be moving forward.
- No longer matches requirements – Requirements change over time and, from a users point of view, so does the software. That of course does not always keen that we’re keeping everything up to date and those trusty core services can quickly become legacy.
- Too hard to change – possibly the people in a team change, maybe the frameworks used stopped being developed. Whatever the reason if it is not well enough documented or tested things do become hard to change. As [Robert Martin] defines it any code without tests is immediately legacy code!
Technical debt – is it the same?
Technical debt is another term that you hear a lot in the software industry these days. It’s quite possible that people outside of Software Engineering have started to consider it their number one reason why a team cannot go faster. But is it the same as Legacy Code?
No. Technical Debt is the term for those things we know that should be improved but either do not have time to resolve or have decided that there are higher priorities (probably from elsewhere in the business). If technical debt is not addressed then it will probably lead to code becoming legacy sooner than otherwise. Capturing technical debt and prioritising the resolution of the items should help to keep your code in better order.
A common cause of technical debt will be due to a prioritisation on releasing quickly or at a given date where the amount of work required to complete properly would not ideally have been fitted into that time constraint. This typically leads to short cuts or a lack of knowledge share which can obviously make software more fragile than if that could be avoided. As mentioned above capturing these items as they occur and coming back to them later should help avoid stepping on the slippery slope to legacy code.
But when can we do the work required to resolve technical debt? Well we agreed that in an ideal world it is scheduled in immediately – allowing the team to learn from their choices and then work to improve the code before it leaves the collective consciousness (perhaps adapting velocity or other delivery metrics to accomodate). If time really is a pressure factor and releases have to be delivered then this may not be possible. In that situation ensure that your team have the space to reflect on the project and make improvements before the next project truly gets underway and we start again.
Complexity increases over time
At this point the group reflected that it may not be due to any fault of the team (current or past) that led to the legacy code or technical debt. Software increases in complexity over time, that is just the truth of it. What we need to be mindful of is to ensure that rising complexity of the product does not lead to increased complexity of the code itself. Doubling the number of features or the complexity of a single feature should certainly not mean that methods become twice as long – probably not even longer files. Always encourage your engineers to be maintaining a manageable source code, breaking out new files and new modules where complexity grows. And for top marks follow Test Driven Development which will help enforce clean code and a “write the minimum required code” attitude which helps to avoid scope creep and unnecessary complexity.
[At this point we discussed MVP and Prototyping which will be the topic of another post]
Make it maintainable
It was agreed around the room that the appropriate strategy for dealing with legacy code is NOT to replace it but to make it more maintainable (that’s not to say that a replacement strategy may not be right at certain points in time). To turn your legacy code into manageable, workable code the approach is to make it more maintainable – but how?
- Add (unit) tests – Before you can make any changes to legacy code with confidence you need to surround it with tests. The most effective tests are closest to the code – so aim for unit tests if you can. As the older code in your platform it is often touched mostly for bug fixing, if this is true for you then try enforcing “test first” bug fixing (or even TDD). That way you will gain test coverage over time.
- A regression suite – Do you have confidence that the UI will continue to work after changes to legacy code? Often functional tests are the first approach to verifying correctness but it can be more helpful to think of this as a second line of defence. If your unit tests pass (fast feedback for development) then a functional test suite can prove a level of confidence that no regressions are being introduced by these changes.
- Identify problem areas – Once you have an approach for adding tests and growing confidence in your ability to change legacy code it’s important to direct your efforts rather than attempting sweeping changes across the whole platform. With help from support desk numbers or usage statistics try to identify areas of your product where there are a high number of bugs, issue reports or where usage is very high and you would like more ability to add features with confidence. Start work in these areas and you will see fast returns for your efforts – and the ability to make incremental improvement.
- Small iteration – As above try to identify the smallest change in the areas you have identified and make the appropriate tests, enhancement and roll it out. This will provide incremental improvements to your codebase and to your users. As long as you can always be making your legacy code better and more maintainable then you’re avoiding the same thing happening again.
- Try virtual machines – If you are struggling to seperate the concerns of your application and finding new areas of functionality are proving unstable by proximity to your legacy code you could consider using virtual machines or containers. Moving the legacy codebase to a sandboxed environment ensures that the boundaries are set and maintained and that limitations of your technical choices do not affect new code. With proper separation the impact of legacy parts of the system should be minimised and will help with any migration plan.
Learn from it
The main take-away from our discussion on legacy code, other than the need to deal with it, is to learn from it. Legacy code is often a fact of fast development, prototypes accidentally being released to production, and business factors causing engineering teams to not have the time to deal with their technical debt. The biggest action you and your team can take in these situations is to learn what could have done to avoid the situation and to put in place methods that will help to keep the correct balance going forward.