So, you want to refactor your legacy system. Great you’ve taken your first steps toward insanity and a wild ride.
So here is the scenario:
You’ve just been tasked with overseeing the technical team that supports and improves a software product that has been around for 7 years. It is currently running in a production environment, and the users are generally happy with the system.
Other developers have that look on their face when you discuss upcoming changes and features that the business wants for the old system. That look, by the way, is despair and terror. As soon as a developer joins the team, they start counting the days until they are reassigned. Nobody’s having any fun.
You decide to look under the hood, the true nature of the beast appears to you, the system lacks any discernible architecture, the development patterns and practices you are deeply familiar with have been ignored with contempt. Instead, what you find is inherited code that you and others find difficult to understand and worse, expensive, and incredibly challenging to change.
Not only that, but when you do manage to make changes or add features, you and your team spend the next sprint fixing the bugs you’ve inadvertently introduced despite your best efforts as there are no tests.
Your first instinct is likely to be that of Ellen Louise Ripley, the fictional protagonist in the critically acclaimed and universally adored sci-fi classic, Aliens:
First Prize, Rebuild
Starting over is way more accessible (and fun); you can frolic in a new build’s green fields and blue skies. Flex with the knowledge you have gained from countless hours of tutorials and blog posts and expert opinion that you’ve absorbed in your career, stand on the mound and smugly showcase what an excellent system should look like. Beautiful, clean and easy to read, following industry expert best practices. The envy of every development team in the organisation.
If this is your scenario, friend, congratulations, you’ve managed to convince the business that 7 years of development and the sunk cost was pointless, and starting over would be better for everyone. They’ve given you free rein to overhaul a decently running system that, before now, chugged along just fine, needing the odd feature and update here and there. Feel free to stop reading now… you’ve won.
Second Prize, Reassignment
Just beg for reassignment. This is the next best thing that could happen. Wouldn’t it be great if you could make this someone else’s mess? All of your stress would be gone, your violent night terrors and shower thoughts could be occupied by a project that didn’t make your palms clammy and throat dry up at the idea of a feature release. If you can get this right, you’ve also won. Stop reading.
Third Prize, Reality
The truth is we all go through this, start over followed by wishing the daunting task of refactoring upon some other bright-eyed, hopeful individual whose worldview has not been tainted by the task at hand. But this is your problem. This system is not correct, and you have a responsibility to fix the past and make the future of the project and your legacy one to be proud of. This is your war, and you will not be beaten.
The business case for a refactor
Making a case for a refactor is not an easy task; there are often many counterarguments and stakeholders that may stand in your way to tackling this task. It is often difficult to sell the idea if you haven’t articulated the goals of the exercise well enough for others to buy into the picture. Simply stating: “the code is a mess, it needs to be updated” is not a good reason to spend time and budget working on improving legacy code when that time and money could otherwise yield product features and tangible improvements.
You need to have clear goals and (good) reasons to support your pursuit. These reasons almost always need to relate to financial benefits for the company. Your first responsibility is to identify and articulate the business value proposition of embarking on an intangible adventure back in time to code that is currently working and explain why potentially destabilising it and delaying feature improvements is to the company’s long-term benefit. You may be thinking this sounds difficult, give me the second prize. Good, it should be challenging. Refactoring a legacy system is not a task for the faint of heart or the uncommitted, and if you cannot find a practical business reason to pursue it, it shouldn’t be pursued. Sometimes code should be left alone.
The core purpose of a refactor
Refactoring is always to fight technical debt by transforming messy code into clean code by simplifying, making it more readable and imparting good practice and patterns into areas of the code lacking these qualities. Always keep this in mind when refactoring code; you don’t want to make things harder; you want it easier and cheaper for everyone.
Rules of engagement
Like any civilised war, some rules define the current state and circumstances, the conditions, degree and manner in which your refactor effort may be applied.
The first rule, Have a clear goal for the refactor
Goals for a refactor are critical; you need a target, defining whether you have completed your mission. As stated, the primary purpose of a refactor is clean code. The qualities of clean code are:
- Easy to read and understand,
- No duplication,
- Minimal moving parts,
- Passes tests,
- And most importantly, cheaper and easier to maintain.
Use these qualities as a starting point when defining your technical goals. Some examples (not to be used as the gospel, of course, each project is different) of technical objectives are:
- Achieve a cadence of new features to deployment to production of no more than 2 weeks.
- New developers onboarded and productive within 1 sprint.
- Easy and quick to identify areas of the code where a change is to be implemented.
- Define an industry-standard architecture for the code.
- Reduce the number of bugs returned by each change/feature by 80%.
- Improve system performance and usability.
- Reduce duplication and remove unnecessary code liability.
The second rule, benchmark the current state of the solution
How will you know if a refactoring effort has improved the code or, in some cases, made it worse? Basing the outcome on a benchmark and measuring your progress to your goals is critical to know if what you have done has been fruitful or just a waste of energy for no real gain.
Use static analysis tools available to you to set a baseline for your solution. They will help you decide where to start, where the most value will be gained, where the effort should be focused first and allow for a mechanism to measure progress. Products we use to do this include SonarCube and NDepend, but others have more features and complexity, some free but limited functionality.
Cool, you have some goals; you know what you’re in for in terms of the system’s current state and where you will start making a dent. This is probably the part where refactors get derailed most often. It shouldn’t surprise you by now to learn that there is an excellent process to success at this point, and it isn’t easy, and it isn’t always quick (or even possible in some cases). The guiding rules of thumb when implementing a refactor are:
- Write tests to support the code you want to change.
- Make sure they pass before you change anything.
- Refactor the code that is supported by tests.
- Make sure the tests still pass.
What if the code is so tightly coupled that it will take so much effort to write them that you may as well rewrite the solution instead of trying to shoehorn unit tests into the code? Well, now it’s not so easy anymore.
You might think, “screw the tests. I know where and what this code does in the full context of history and this complex solution; I won’t introduce any bugs or break anything; I’ll just continue refactoring and trust in my abilities.”
If this is you, you’re a statistical outlier and probably worth way more than currently being paid. You should speak to your manager about a raise. For the rest of us mere mortals that rely on the fallible coagulation of meat and fat in our skulls, this seems a little risky.
Some test harness is better than no test harness
Start with at least some level of testing; in the case of a web application, you could introduce some automation testing using a product like Cypress.io to verify the end-to-end functionality of an area targeted by refactoring is stable before and after changes.
Break dependencies to make unit testing easier
While it is possible to write unit tests for tightly coupled dependent systems, it is often more work than to break the dependencies and write unit tests against a loosely coupled codebase dependent on interfaces. So, this should be the next step before changing any low-level logic. Once the code dependencies are abstracted, unit testing is much much more manageable. But be pragmatic; don’t test everything.
Implement the refactor
The area you want to change is tested and verifiable. You focus on the most valuable part of the code based on the static analysis of the existing system, and you can measure whether things have improved. Now you can go ahead and make the code changes required to meet your stated goals.
Don’t tackle the whole project at once. Only refactor valuable areas of the code that are well supported by tests. This will reduce cognitive load and stress on the development team by mitigating unknowns and ensuring minimal bugs.
Release refactored code early and often. The more often the code is released, the less likely it is that refactored code will have significant side effects as each release is relatively minor and well tested.
Measure your progress to your stated goals
Suppose you decide to use a static analysis tool like NDepend or SonarCube. In that case, you can measure the refactor against the benchmark to show where the code has improved following the standards and rules set out in the static analysis.
Refactoring is a cycle and is deemed complete when the goals are met. If your goals are met, you should stop the refactor.
Hopefully, you are now able to confidently begin a daunting refactor. There are excellent resources on the internet, much smarter than me, that will give you specific pointers on each topic covered in this post and more. Know that you are not alone in this; all successful software eventually has to face the prospect of a refactor at some point.