Any software developer knows the need to add a new functionality or a bug fix to legacy code. Often times, the code we aim to change has been written by someone else, is overly complex and, more times than we would like, lacks adequate test coverage. This is why refactoring is an indispensable tool for any developer. This article will focus on refactoring best practices based on my experience, mostly as Front-end developer, and also on Martin Fowler’s book: Refactoring – Improving the Design of Existing Code (that I strongly advise you to read!).
I believe that many of the things I write here are also the result of my personal opinion, but I have no doubt that they can work as a recipe for success when dealing with big programs, programs in production and where a failure or bug can lead to large damage and costs. Anyway, it’s okay if you don’t agree with everything 🙂
What exactly is “Refactoring”?
According Martin Fowler, “Refactoring is all about applying small behavior-preserving steps and making a big change stringing together a sequence of these behavior-preserving steps. Each individual refactoring is either pretty small itself or a combination of small steps.” In other words, we shouldn’t spend much time having our app in a broken state. We should be able to stop at any moment even if we haven’t finished refactoring. Working in small steps will allow us to go faster because they compose well and crucially, because we don’t need to spend any time debugging.
Debugging is hard and every developer around the world can tell a story of a bug that took a whole day (or more) to find despite the fix being very quick.
Without refactoring, the internal design of software tends to decay because as people change code, often without a full comprehension of the architecture, the code loses its structure. Regular refactoring helps to keep code in good shape!
Why should we refactor?
In regards to making software cheaper to modify, we could divide a long function into small ones. The programs that are easiest to maintain over a long period of time are made of short functions. Each function should be a unit piece of a system and should do only one thing.There is a lot more we can do to improve our code’s quality like, decompose conditional statements, split loops, create Factory Functions, and others, but that’s not the point of this text.
Sometime we can confuse refractor with performance optimization, as both involve code manipulations that don’t change the program functionality but they are different. Refactor can speed things up or slow things down, (and that’s not a problem if your code still meet the requirements), and optimization only cares about speeding up the program turning sometimes the code harder to read!
When should we refactor?
No one is perfect and every developer makes mistakes but, we should always try to leave the code better than when we found it. We must do refactoring as part of adding a feature or fixing a bug. We have to be opportunistic and do refactoring together with other tasks. Of course, sometimes we don’t want to spend a lot of time distracted from the task, we are currently doing, but also don’t want to leave the trash lying around. So, if the improvement is easy to make, we should do it right away. If it’s a bit more effort to fix, it’s better to make a note of the needed change and improve it a bit each time we dig into that code.
Most of the time, refactoring should happen while we are doing other things. This doesn’t mean that planned refactoring is always wrong, but situations like spending a week refactoring should be rare and only when the team neglected refactoring before. That’s the only time when we ask Product Owners or Managers, who most of time don’t understand as well as us why we need to do a refactoring, to create some Refactoring tasks/tickets.
Also, don’t forget we have to refactor when we run into ugly code – but excellent code needs plenty of refactoring too. The task of maintaining software is never done! Refactoring should be part of the natural flow of programming.
Why tests are so important when we do refactor?
One of the key characteristics of refactoring is that it doesn’t change the observable behavior of the program. If we perform the refactorings carefully, we shouldn’t break anything – but what if we make a mistake? Mistakes happen but they aren’t a problem if we catch them quickly. Because refactoring should be made up of small changes, if we break anything, we only have a small area of the code to look and find the fault. To do this, realistically, we need to be able to run a test suite on the code. A suite of tests is a powerful bug detector that reduces drastically the time to find problems.
How to write good tests?
Despite that, tests are also code and can be badly written. As a Front-end developer, when I add or modify some tests I like to focus on the point of view of the user and not on implementation details. In other words, in almost every case, the tests should work the same before and after refactoring because when we refactor nothing should change from the user’s perspective.
Look into the following example (code sandbox):
It’s just a simple React component and the only thing it does is to show a count value increased every time we click on a button.
Now let’s see a simple test for this component (Test A) using Enzyme:
As you can see, it’s calling directly the method “onClick” meaning that if we do a refactor and change the name, for instance, to “increaseCount” we will need to update the test, despite nothing changing for the user. That’s not a good practice and we see it often!
In this case, a better approach would be to use the “simulate” method from Enzyme API because it’s more in line with the user’s behavior.
Now, if we rename the “onClick” method we will not need to update the test and we will get immediate feedback if our change is working correctly.
Just as a quick note, I have had a great experience using Enzyme over the past few years testing React applications but lately I’ve been using the Testing Library. Is made to help developers write tests focused on behavior and not in implementation which helps a lot when we are doing some refactoring.
Refactoring should be opportunistic and composed of small changes. You should avoid having your app in a broken state. Also, don’t forget that a solid suite of tests are indispensable for a proper refactor. Write tests focused on behavior and not on implementation. If you don’t have any tests, you should start adding them and remember: it’s better to have one test than zero tests. You will end up with a robust code!
Written by Pedro Costa • March 11th, 2020