Code Refactoring and why you should refactor your code
Software does not expire, but it “rots”. Its quality degrades over time. As you build your project and add features, you probably won’t always build it in a clean, orderly and mindful way. Especially if you have a tight deadline. So aside from features, you also produce bugs, code smells, and technical debt. That “rots” your software, but your job as a software engineer is to maintain its “freshness” while building on top of it.
In this article we’ll touch on the topics of code refactoring, code smells, technical debt, why/when you should refactor your code, and how to measure the effectiveness of your refactoring. And if you’re looking for this blog post in video form, check out this short 7-part series (~15 minutes total) on code refactoring.
Code Refactoring is the process of analyzing the existing codebase, identifying technical debt and code smells, and modifying the specific code so it’s more optimized and removes the debt and smells, without changing its user-facing behavior.
Refactoring aims to improve the internal quality of the code while preserving its functionality. The primary objectives of code refactoring include:
- Readability: Code refactoring techniques, such as renaming variables or methods, simplifying complex code blocks, or applying consistent formatting, make the code easier to understand and maintain.
- Maintainability: Code refactoring eliminates code smells (e.g., duplication, long methods, excessive complexity) and improves the overall structure of the code, making it easier to modify and extend in the future.
- Reusability: Code refactoring promotes the extraction of reusable components or patterns from existing code, making them more modular and independent. This can lead to reduced development effort in the long run.
- Optimizing Performance: Code refactoring can help identify and eliminate performance bottlenecks by optimizing algorithms, data structures, or resource usage. This leads to more efficient and faster-running code.
- Enforcing Coding Standards: Code refactoring provides an opportunity to enforce consistent coding standards and best practices across the codebase. This improves the overall quality and maintainability of the code.
It’s important to note that code refactoring should be done incrementally and with the aid of automated tests. This ensures that any changes made to the code do not introduce new bugs or regressions.
Code smells are symptoms of inferior code quality that can contribute to technical debt. They happen when you step out of the coding standards and create structures that go against the design principles that you’ve previously agreed upon. Code smells aren’t bugs. They won’t crash your app. They’re just characteristics of poorly designed code that make it tightly coupled, hard to work with, or too rigid to be reused. Does your code have a lot of duplication? Are your functions very long and is hard to figure out what exactly they do? Those are some examples of code smells. They contribute to technical debt, and can leave room for bugs in the future.
Technical debt is the increased cost of maintenance caused by picking quick and easy but limited solutions instead of better ones that take more time to implement. Just like a monetary debt, technical debt accumulates “interest”, making your codebase harder to maintain. Realistically, it’s something that occurs in almost every project, and sometimes purposefully introducing technical debt is a conscious decision in order to push the project faster in order to pull off a deadline. Some examples include hasty or incomplete implementations, lack of documentation, insufficient test coverage etc… You could imagine how these things can keep piling up over time and cause all sorts of issues down the road.
Refactoring your old code is a good practice because it saves you from countless headaches in the future. Just because some code works doesn’t mean it’s good code. Sometimes the software will work, but it will either be limited in performance because it’s written in an unoptimized way, or it will be harder to add new features to it because it’s overly complex and without any rules. A lot of the time it’s both. Taking the time to refactor your code is always a good idea. You will end up with a much cleaner code that is easier to maintain and upgrade, and also remove a lot of opportunities for bugs.
Putting rules in place will help you ensure that the cleanliness and order of your code will remain intact as your project grows as well. Use linters. Decide on coding standards. Mind the formatting. Also, a well written code is a performant code. Have you written or used a super slow app that still performs its tasks? Technically, it does work, right? But it’s a pain to use it. That’s a very big reason to refactor it.
There’s no alarm that goes off when it’s time to refactor your code, but there are indicators that you should be mindful of that will tell you it might be time for some code refactoring. Let’s see some of the indicators and opportunities when you can do some refactoring:
If you find yourself writing similar code multiple times, it might be a good time for a refactor.
There is a principle in software development named DRY, short for “Don’t Repeat Yourself”. It suggests replacing any repetitive information that is likely to change with abstractions that are less likely to change. The principle states that “every piece of knowledge (read implementation/feature/logic) must have a single, unambiguous, authoritative representation within a system”.
It doesn’t mean that you should abstract everything that might be repeatable in order to keep it as a single representation. Be aware that abstraction is not a silver bullet, but a double-edged sword. Too much abstraction will also make your code confusing and vague, thus making it harder to maintain. This too can produce code smells, which is the exact opposite of what you wanted in the first place.
As you can see, you need to find a good balance when abstracting code. There’s an article by Kent C. Dodds that explains a good middle ground between duplicating and abstracting. He calls it “AHA Programming”. AHA stands for “Avoid Hasty Abstractions”. The article summed up in one sentence is “don’t be afraid to duplicate code until the similarities scream at you, then you’d abstract them”.
Multiple, or repetitive bugs in the same domain within the codebase. This means that most likely the code within that domain is of low quality, so refactoring it would definitely be a good idea.
Using an application performance monitoring service like Sentry will help you in figuring out if there are clusters of bugs around the same domain within the codebase.
If you’re adding a new feature and you notice that the code you’re interfacing with is someone else’s dirty code, refactoring it will of course clean it up so it’ll be easier to work with, but it’ll also help you understand that part of the code better.
Same goes when fixing bugs as well. When you’re fixing a bug, you’re actually refactoring old faulty code. You could just patch it up and call it a day, or you could go the extra mile and do a bigger cleanup around the parts where the bug occurred.
Another opportunity to do some code refactoring is during code reviews. This is your last chance to clean up the code before it goes into main and becomes technical debt over time. Take the time to understand the PR and if it’s not yours, point out the refactoring opportunities and even leave a few suggestions.
A lot of things. Last-minute changes can cause technical debt. Unclear requirements usually cause technical debt. Lack of awareness of technical debt can actually cause technical debt. Let’s explore some more in better detail:
- As mentioned previously, tight schedules and deadlines usually do. A lot of times you could be thinking of releasing sooner to be either faster on the market with the new feature, or there’s a marketing event scheduled and you need to get that feature in production ASAP. Being put under pressure like that would force you to use the easiest and most convenient solution rather than spending time to thought-out a clever solution.
- Not having coding guidelines or standards implemented in your project. This is a recipe for disaster especially when working in teams. If there are no standards in place, and everyone is allowed to code as they like without structuring their code, what do you think the project would look like after some time? Having coding guidelines or standards put in place is a must-have for all projects. Use Wikis, Linters and Code Review to define and enforce the coding standards that you and your team would agree upon.
- No code reviews, or postponed refactoring. Technical debt is removed by refactoring the old code. If there are no code reviews in place, or the refactoring is postponed for any reason, then the technical debt will just keep piling on.
- No ownership and leadership. The “lead developer” title exists for a reason. Not having a lead developer on the team, or a senior developer (the title doesn’t need to be official) will make it easier for the technical debt to “pass-through” the less-experienced team members’ filters. Lead/senior engineers will also enforce coding standards and maintain quality code reviews.
- Not having tests in place. This leaves a lot of room for “band-aid” bug fixes to go unnoticed and contribute to the technical debt.
You could take the previous section “What causes technical debt?” and do the opposite. No really. I wrote them in a way that will also give you an idea on how to avoid technical debt. But let’s make an actionable list anyways:
- Agree upon coding standards from the very beginning, document the standards and enforce them while coding. Make sure to point them out during code reviews.
- It’s very important to do code reviews. Very important! A code review is your last chance to catch potential bugs and code smells before they get merged and contribute to the technical debt.
- Have tests in place. The tests will prevent quick and dirty fixes. It’s very easy to decline a PR whose tests are failing.
- Don’t avoid or postpone code refactoring. Refactoring code is your “undo” action for pushing code smells and dirty fixes to the main branch. Some code smells can slip through the code reviews as well. Also, as time goes by, new versions of your dependencies are published which could contain breaking changes, requiring you to refactor your code before upgrading them.
- It’s important to have a lead/senior engineer on the team that will enforce good practices for maintaining the quality of the software. Maybe you can’t just assign that role, but you can always discuss it with your team or make a suggestion to your manager.
- Remember to take your time and thoroughly think through your solution as you develop it. Think about important use cases that your part of the code will affect. Do a self-review before you open a PR. You can use the Git Diff tool to highlight your changes, which makes self-reviewing your code easier.
There are multiple metrics you can use to measure the success of your code refactoring. You can use Codecov to monitor your code coverage. Code coverage tells you what percentage of your codebase is tested and can be trusted. Of course, a more extensive code coverage means a more trustworthy codebase. Since code refactoring changes your codebase without changing any functional behavior, having proper tests and having good coverage is crucial for ensuring that your refactoring was indeed successful.
Maybe your refactoring also fixes some bugs and/or improves the performance of your application. In that case, you can use Sentry’s Release Health and Performance Monitoring features to measure the percentage of crash-free sessions and your app’s performance to determine if the code refactoring did in fact decrease the bugs and/or improved the performance.
We’ve also used Sentry to verify a large refactoring in Sentry before, and you can too. In the article, Mark explains how he and his team created “fake errors” that got reported every time an old part of the code that shouldn’t be reached got reached. After their initial refactor, they pushed to production and waited for those specific alerts. With a clever use of manually capturing errors, and configuring custom alerts, you can build your own refactoring verification tool using Sentry.
There are also other metrics, like measuring the time taken to implement a new feature or fix a bug around the code that was refactored. If the refactoring was successful, it should take less time to implement a feature or fix a bug. You can also request simple feedback from the team on their opinions of the effectiveness of the refactoring effort. Their insights can provide valuable qualitative assessments.