Once upon a time, there was a good software engineer whose customers knew exactly what they wanted. The good software engineer worked very hard to design the perfect system that would solve all the customers’ problems now and for decades. When the perfect system was designed, implemented, and finally deployed, the customers were very happy indeed. The maintainer of the system had very little to do to keep the perfect system up and running, and the customers and the maintainer lived happily ever after.
This story sounds nice, but little could be further from reality. Software changes, because of requirement changes, design changes, code changes (e.g. bug fixes), technological changes, or even social changes. There is plenty of reasons that would cause your code to evolve quicker than you expected. The same is true at Dashlane. And to keep our code in great shape, we have made refactoring a core part of our engineering routine.
What is refactoring?
“Code refactoring is the process of restructuring existing computer code without changing its external behavior. — Wikipedia”
Refactoring plays a major role in software evolution by making dated software reusable (some frameworks even include it in their process like Extreme programming). Unfortunately, refactoring is commonly done completely siloed from the rest of the development cycle. For an engineer, reading code that hasn’t been refactored is akin to reading a novel that hasn’t been properly edited: it’s difficult to understand, time-consuming, and frustrating. Something as important as this should not live on the edge of your workflow.
Why do you have to refactor code?
There are many reasons that help explain why refactoring is so important. First is the difference between knowing and doing. Most software engineers know, at least to some degree, what are best coding practices. However, tight deadlines, compromises on scope, MVPs, and human error can get in the way. But what seems small in the moment can compound over time.
The second reason is that all code eventually becomes legacy. Because requirements change, design needs change. It can even starts with complacency, nothing important. Just the update of name of a third party app (i.e. Appboy became Braze in 2017) that is not reflected in the code directly. At the moment, you don’t think that it is important, “yes we all know that it is Braze.” But after two years, new members join the team. They won’t understand what Appboy is. And once you’ve explained them, it will in any case require more focus to read the same piece of code because people will have to map Appboy to Braze while reading. This example is small and harmless, but the pain compounds with each additional needed change. Suddenly, what should have been easy to read for someone new to your codebase always requires someone who was there when it was written.
When and what should I refactor?
All code eventually becomes legacy, meaning everything eligible for refactoring. But with finite time and resources to dedicate to refactoring, we prioritize any code that falls into one of the following three categories:
- Too many bugs: Any part of the system that has a disproportionate number of bugs
- Too hard to understand: Any code that is so confusing your team struggles to fix bugs
- Too hard to change: Any module that is so complex to update (circular-dependencies, lack of tests, duplication…) that your team can’t improve your software.
What should I refactor?
Once you’ve identified which part of the code should be refactored, it’s time to find what specifically to refactor. For this, look for code smells. A code smell is any characteristic in a program’s source code that possibly indicates a deeper problem. Determining what is and isn’t a code smell is subjective, and varies by language, developer, and development methodology. There are dozens of code smells! Duplicated code, bad naming, long method, long parameter list are examples of code smells but you can refer to external pages such as this blog post to have a complete list.
When it’s not a good idea to refactor
There are times where an application needs to be completely revamped. Refactoring may be unnecessary when the time to fix a bug or develop a feature becomes unreasonable (a measure that depends on your team’s resources and the application’s size) or when your application is written in a old language that nobody knows anymore. In these situations, it may be more efficient to rebuild from scratch.
Another situation in which it would be wise to skip refactoring is if you are trying to get a product to market within a set time frame. In this context (MVP, A/B test, etc.), speed to production should be prioritized above refactoring. Refactoring can be like going down the proverbial rabbit hole: Once you start, it can become quite time-consuming. Adding any additional coding or testing to an already tight timeline will lead to frustration and additional cost for team.
Code refactoring best practices
Even if refactoring can take many forms, there are best practices that should be followed and in most cases will simplify your work.
- Test, test, test: You won’t be able to efficiently refactor if you cannot make sure that the behavior before and after the refactoring is the same.
- Small steps: Split refactor into small steps. Check after each step that your code behaves identically by running tests. One refactoring leads to another. Don’t forget that major change requires many refactoring steps.
- Refactor first before adding any new features: There’s a reason why refactoring is defined as changing code without changing its external behavior. Avoid the temptation to introduce new features at the same time as refactoring. Introducing a new feature requires creating new tests your refactor must pass. At Dashlane, we work on product changes independently of our refactor (ideally afterwards, to make the job of building the new feature much faster and cleaner!) so we can best understand the code.
- Plan your refactoring project and timeline carefully: The most important outcome of refactoring is that not only is the code cleaner but that it works. And remember, it’s going to take longer than you think, so plan accordingly.
- You can perform a refactoring != You should do the refactoring: As developers, we are focused on writing better and clearer code. However, it might not bring much value to the product we’re building, or the customers that count on us. Technology serves a goal and doesn’t have an added value in isolation. Make sure that refactoring aligns with business goals and brings value to your end users. Focus on progress, not perfection (as said before, all code eventually becomes the dreaded legacy code).
How to avoid a refactor that takes forever?
We’ve all heard about a refactoring project that took 10x the expected time. The best way to avoid this scenario is to practice continuous refactoring.
Some teams are still cautious about this practice because they see the risk of change. What they don’t see is the risk of keeping legacy code and the cost of building technical debt with a series of workarounds.
At Dashlane, our Engineering teams try to live by the mantra “leave it better than you found it.” All our coding practices (clean code, review, ownership) aim to limit the cost of software evolution, and continuous refactoring is no different.
It might be hard to dedicate time to refactoring in real life
As you know, real life happens to be different from expectations (and blog posts). The realities of your business might drive a wedge between your desire to refactor and your ability to do it. One of the main obstacles is that refactoring doesn’t bring user value right away, which in the short-term means there’s not a compelling business case to bring to your peer stakeholders. From my own experience, here are some piece of advice to defend you refactoring project:
- Firstly, if you’ve identified the piece of code you’d like to refactor, you should clearly be able to say what the added long-term value will be. It might not directly impact customers, but a proposed refactor has to have a clear benefit to someone. When presenting your idea, it is crucial to set a scope and a time frame. And remember, it’s going to take longer than you think, so plan accordingly and give yourself a little extra cushion of time.
- Another thing that should not be left behind is to establish trust between engineering and product teams. It is important to have all teams aware of the cost of technical debt. Technical debt doesn’t appear only with bad developers and can have a tremendous cost.
- Finally, only refactor what’s essential. Avoid doing positive procrastination (refactoring something for its own good). If you request time for a refactoring that takes longer than expected or which was not required (both makes it the worst case scenario), you’ll progressively lose trust from your product teams and stakeholders. Which will make it more complicated for you to defend your next refactoring project.
Make sure that nobody forgets the saying: if you think that good architecture is expensive, then try bad architecture.
This post can be distilled to five main points:
- Refactoring is about changing the factoring—without changing its external behavior
- Use tests to enforce the validation of the same behavior of your code before and after your refactor. It will help you to move faster
- Split refactors into small steps
- Dedicating time to refactoring can be done only if there is a trust relationship between teams
- You can perform a refactoring != You should do the refactoring
I hope you found this helpful. Have refactor tips you want to share? Comment below!
Want to participate to our continuous refactoring? Check out our careers page, we’re hiring!