Rescuing web apps (part 1)
Most programmers have the best of intentions. We all like to produce code that: a) nails a particular feature; b) runs quickly; and c) reads well. Despite these intentions, we've all ended up in the situation where our code has degraded into something ugly, unperformant or useless (aliteration unintentional). Sometimes it's out of our hands, e.g. when a client decides that yesterday's critical feature is no longer on their agenda, leaving redundant code littered through our project. Sometimes it's preventable, like when we gradually add tiny bits of code without refactoring to adopt it, or without introducing new tests. However it happens, it's an inevitabilty that we need to accept. In fact, rescuing projects from these situations help us learn and grow as developers.
I, like a lot of programmers, have a streak of perfectionism. When some code starts flashing red warning signs, our overwhelming feeling is to throw it out and start again from scratch. We think something along the lines of, "well that ended up badly, but this time the code will be PERFECT". This is fine on a small scale, such as a handful of classes or methods. But when a whole project ends up in this situation, the decision to scrap the existing code and start from the beginning is not one that should be taken lightly. I've seen many cases of project rewrites that never get out of the door, often hindered by perfectionism that came about as a response to the failure of the previous version.
We shouldn't be scared of refactoring. It's an inevitable part of the development process, and trying to write perfect code that never needs a refactor is both impossible and paralyzing. On the other hand, we shouldn't be scared of rewriting, and shouldn't dismiss the idea entirely; the difficult thing is to know when to do which.
In this first part of my guide to rescuing web applications, I'll give some pointers to help you determine what state your project is in and therefore whether it's better to refactor or rewrite. In the second part I'll give some tips on how to do both of these on a practical level.
1. Work out what's gone wrong
Obviously, you can't set out fixing a problem before you've worked out exactly what's gone wrong. There are many reasons why a project can turn sour, but here are a few of the most common ones:
- The feature requirements suddenly change to the point where the existing project needs a huge overhaul.
- Months of sub-par development or shortcuts has lead to a spaghetti mess of code.
- The test suite is poor or non-existent and bugs are coming thick and fast, hindering the team's progress.
- The framework(s) that the project is built on becomes obsolete, or becomes unusable for the purpose of the project.
- The project is built on an outdated version of the framework, where updating would create many breaking changes.
- The project isn't built on a framework at all, and is now unsustainable.
In my opinion, numbers 4 and 6 are the only ones in this list that pretty much require a project to be rewritten. It's very rare that you can move code between two web frameworks, and I've not yet seen a successful and relatively complex web application that isn't built upon a framework (however, I'm not claiming that it's impossible).
All other situations require a careful audit of your project before writing any healing code.
2. Make estimates for refactoring and rewriting
Consider both options equally. How long would it take to rewrite the project from scratch? How long would it take to refactor the existing code? Take the following into account:
- Database schema: is it quicker to build a new one, or modify the existing one?
- Controllers, views, models, tests, supporting classes. We often overlook things that work in a "problem" project, and focus on the nasty areas. That can mean that we vastly underestimate how much time it took to create all these different parts in the first place. Creating them from scratch could take a lot longer than you think.
- Is the existing test suite useful? Even if it has problems, think very hard before throwing it away, as problems can be fixed.
- How long would it take to set up a completely new project? We often vastly underestimate this.
- Is the project's design and aesthetics getting in the way of your decision? An ugly site can be turned around very quickly but, in my experience, has a considerable effect on our mental state as developers on the project.
3. Be honest with yourself
Admit your biases. A rewrite feels "cleaner", but time is pretty much always the biggest factor in a situation like this. How long would it take to get a rewrite to a usable state? Would you have to maintain the existing codebase alongside the rewrite? If so, that's a huge undertaking, and there's a likelihood that the rewrite will never be finished as the two projects diverge.
If the project already fullfils the needs of the user or client and your issue is purely with the quality of the code, is your desire to rewrite the project a personal crusade? If the app does what it's supposed to do, that's the perfect scenario to refactor the codebase piece-by-piece.
Our preference is always to write new, beautiful code over maintaing ugly, messy code. But if we're truly honest with ourselves, it's often quicker to refactor than rewrite. This becomes more true the longer the project has been running, unless it gets to the point where a large proportion of the features are no longer needed.
The bottom line is to be practical and realistic when weighing up the two options, and don't let your idealism get in the way.
4. Make sure the rest of the team is with you
If you're the decision maker, don't push ahead without getting the support of your team. Put forward your idea and the reasons behind it, and listen to the opinions of others. This is true for pretty much everything, but definitely applies in this case. A large-scale refactor or total rewrite is a big deal and the whole team needs ownership of the idea, otherwise demoralising moments will be much tougher.
If you're not the decision maker but feel like a bad decision is being made, speak up! If you don't feel like you can then that's a separate and more serious environmental issue.
5. Set achievable goals
I'll discuss this in more detail in the next couple of parts, but break the project down into small, measurable chunks of work. Release the project (externally if possible, but at least internally) after each chunk. Don't make your goal "rewrite the project". Collect as many metrics as you possibly can on the quality of the code; this is particularly effective when refactoring, as it's really encouraging to see code quality and test coverage metrics climb as the project improves.