Refactoring web applications
I’ve been thinking a lot about refactoring recently (rewriting functional code to improve its quality, security, legibility etc.), particularly in relation to some projects I’ve been working on. This morning a twitter thread by Sarah Mei (@sarahmei) caught my attention and I thought I should finally write something on the subject.
I’ll come back to Sarah’s thread throughout this post, but it’s well worth a read and takes a different angle to what I’m about to say. My main talking point from it is that incremental refactoring is essential to a healthly project, and we don’t need to make massive changes all the time. But she also talks excellently on analysis and communication.
To actually fix it, you need to negotiate with the individuals who are applying the pressure [to not refactor]. You need to understand THEIR incentives, and align your desired changes with those.Sarah Mei (@sarahmei)
Like many (most?) developers, I’ve spent a lot of time with “bad code”, most of which I’ve written myself. This is code that I know to not be high quality, and that I understand needs to be refactored. But I want to identify first what makes code “bad” and why we put up with it.
There’s a category of bad code which is just poorly written, perhaps because of laziness or lack of skill. But that’s a tiny proportion of most of the code I’ve ever refactored. In my experience, most bad code was fine when it was originally written but has since turned sour, for a variety of reasons:
- Perhaps best practices has moved on and what was best practice is now bad practice
- Perhaps the scope has changed and what was a small throwaway feature has become a fundamental part of an application
- Perhaps you originally made a rough proof-of-concept and it’s been blindly embedded into a real-world application
- Perhaps dependencies have been upgraded and you’re using a package which is missing key features or, worse, isn’t being supported
- Perhaps you’ve found a better way of doing things and you’re suddenly aware that the code could be faster, smaller or more pure
- Perhaps your code has become insecure due to a vulnerability being discovered, or processing power evolving to make brute force attacks possible
All of these circumstances can be mitigated slightly with good preparation, but I think it’s naive to think of them as completely avoidable. Writing web applications is a constant stream of being blindsided by technology, market trends and user needs. So, at some point, your code will become bad and need to be refactored.
I want to talk a bit about my experience lately and where I believe I went wrong. Then I’ll talk a bit about what I’ve learnt from the experience and what I’ll do to avoid it happening again on my next project.
About six months ago I picked up a new front-end project, based on React and Redux. The project was started a couple of years back, and has been intermittently worked on since, but I believe I was the first developer to really start looking at it on a day-to-day basis with the vision to release it to customers.
If you’re already seeing the red flags, then you did a better job than me. I picked up the project, got a feel for how it worked, and started implementing new features. Refactoring didn’t really cross my mind.
What I missed was that the project was littered with bad code. And let me clarify again that this wasn’t because of anyone who worked on it previously, it was because:
- The project had been started two years ago
- It had only been worked on intermittently
Two years had been a long time for the React ecosystem, and best practices had moved on significantly. The project had been started from a popular-at-the-time template which structured code and used packages in a way that is now discouraged (I tracked down the original template, which explains it is now deprecated and not recommended).
Over the course of two years, work hadn’t been properly planned or scheduled (the application was effectively a prototype during this time) so new features were introduced sporadically and inconsistently. With the time constraints this is excusable, but it left the application fragmented.
Ultimately, coming into the project in mid-2018, the code was inconsistent and didn’t match any reference sources online (since documentation is updated to reflect current best practies) and a lot of the dependencies were out-of-date. Some of those dependencies could be upgraded quite easily, but some (for example, upgrading React Router from version 2 to 4) required complete code changes.
A little while into the project I started to appreciate the scale of refactoring required and quickly pushed the thought from my mind because it was “too big”. I had features to implement, deadlines to meet, and I didn’t have time to rewrite data architecture (a scarily impactful change!).
But reality shortly set in. Some features required packages to be upgraded, and documenting a system which mixed old and new techniques was becoming a nightmare. I started to pick a few battles and refactor the application as-and-when I came to problem areas. In effect, I was starting to read the advice I saw from Sarah today:
Consistency.Sarah Mei (@sarahmei)
Six months into the project, there’s been a lot of improvement. Our usage of Redux is (mostly) clean and consistent, we’re starting to build up a component library, ESLint is strict and passing, and this week our React Router dependency got upgraded to version 4.
However, there’s still a lot to do: there’s some data duplication across Redux stores, directory structure is unintuitive and there’s a 2000+ line component that I’m dreading having to tackle.
As I see it, incremental improvement to the application was the only way to keep this project working. Avoiding these small refactors would have led to a slower, harder to develop application; a large refactor would have meant throwing out a lot of our good work and delaying the project. Instead, we have a functional, better but imperfect codebase, happy clients and a good vision for future work.
Improvement over consistency.
I’m well aware of the mistakes I made on this project. I didn’t acknowledge the need to refactor quick enough, didn’t communicate this to my colleagues and managers, and didn’t suitably plan how I would perform the refactoring. So, as ever, let’s make this a learning opportunity. What will I do differently next time?
Firstly, I’ll make sure I properly assess bad code when taking over new projects or at regular intervals when working on my own. There’s no shame in acknowledging that code falls out-of-date and identifying where improvements can be made.
However, I’ll be careful in this process not to over-estimate the bad code. It’s easy to get carried away and insist on making sweeping changes which won’t actually help (e.g. migrating to the hottest new tool when your existing one works and is actively supported).
Assessing the bad code will also involve writing a report about my findings. I’ll identify the isolated areas of the code which need to be refactored, make suggestions about how they can be improved, and provide options for how that might be approached.
I’ll share the report with colleagues to raise awareness of the problems and indicate what additional work might need to be prioritised. They may be able to suggest solutions they’ve adopted in the past, or identify other projects which could benefit from the same refactoring.
Doing this proper analysis and report writing will ensure that I’m aware of the refactoring opportunities, and can properly plan for how to enact them. When I have capacity, or am working in a related area, I can refer to my report to pick up the additional refactoring work without having to do all the analysis again.
Finally, I wanted to end with a brief note about how to ensure that you suitably communicate the importance of refactoring. In the introduction I suggested that refactoring involves rewriting “functional code” (which isn’t high quality), and people can often balk at this idea: if the code is functional, why waste our time rewriting it when we have other things to work on?
Again, Sarah’s Twitter thread covers this very well.
There’s almost always a win-win in there SOMEWHERE. You can start by trying to understand what is driving that desire for them. It might not be what you think.Sarah Mei (@sarahmei)
Refactoring will often bring tangible improvements: faster, smaller, fewer errors, looks nicer. If these match with your colleague’s/manager’s/company’s priorities, then you have a great opportunity to sell the benefits and get buy-in.
But some refactoring isn’t like that. It can have other reasons:
- Using an old version of a depedency may work but it won’t be supported if something goes wrong
- Outdated or complex code is hard for new starts to understand
- Old technology is battle-tested, but hard to recruit for
- Inconsistent code is confusing, and damages developers’ cognitive load
- Complex code may work, but it’s fragile
Importantly, all of these reasons to refactor can save time and money in the future because they allow you to mitigate against anticipated problems. So whilst code might not be causing you problems right now, spending a few hours to refactor something today might save you five days, apologetic emails and wasted money in six months time.
Ultimately, when agitating for refactoring, you need to make sure you know why it’s a good idea, and that you can sell that to your team. Identify the reasons, calculate the impact, and explain your strategy to mitigate.
Allow me to finish with one final plug for Sarah’s Twitter thread. It covers a lot more topics than I go into here (I’m writing this to selfishly talk about my work) in a thoughtful and practical way. If this post was interesting to you, Sarah’s thread certainly will be too!