The refactoring process is an integral part of software development, both for small projects and international solutions. You can refactor code for as long as you like, but it is important to stop in time to prevent the complete project rewrite.
What is code refactoring?
The definition of code refactoring is familiar to everyone who has to deal with legacy code:
Refactoring is an umbrella term for all the methods of improving existing code without affecting its behavior or functionality.
There are many reasons for code refactoring. Let's start with the most obvious ones.
Imagine you develop a new feature that takes too much time and goes beyond the initial estimations. You’re upset because you expected a completely different behavior from the source code, namely that the previously written solution could be easily extended or modified.
At the same time, maintenance of the legacy code is complicated because of numerous "hotfixes," and even if the source code is flawless, its logic might be different from another developer's logic. The first solution you might think of is to refactor the code from scratch.
This is a pretty trivial example, but let's look into it further. In the case above, code refactoring is done to save developers time on delivering new features. Once done with it, they might go on to refactor the rest of the code with the goal of delivering features even faster.
As good as it sounds, there are potential pitfalls. Let’s look into the pros and cons of refactoring.
Refactoring pros and cons
Let's start with the pros:
- The code looks amazing (according to developers).
- The next time you have to rewrite this code, it will take less time to investigate the issues.
- There are fewer bugs and more predictable behavior (if the code was refactored to improve performance).
Let’s move on to the cons:
- Refactoring takes time, which converts into money. Customers might not have enough money or want to invest in code improvement. Business is not interested in the beauty of the code, it's interested in the result. Despite how hard you try to prove that code refactoring will speed up the development in the future and help other developers to maintain it more efficiently, customers won't take your words seriously.
- Refactoring can be different. There's indeed a useful process of code improvement, but developers don’t always implement it due to a lack of expertise. A common case is when developers refactor just for the sake of refactoring.
As you can see, there is no obvious answer to: "Refactor or don’t refactor?" but in most cases it’s worth implementing. At this point, you should not overdo it in order to avoid losing precious time, as well as the customer's money.
When and how to refactor code smartly
Martin Fowler describes the process of refactoring and its best practices in his seminal book Refactoring: Improving the Design of Existing Code. I advise you to read this book since it lays down the main principles of the "good code" and explains how to prevent the "ugly" code. We will discuss a couple of things from the book in the context of web development below.
Know when duplicate code is justified
Any student who sees duplicate code will tell you that it must be removed or unified. You might say, "Well, you're right," but there are cases where it is strongly advised not to do this.
Let's look at a couple of examples.
One of them is an excerpt from a speech by Ihor Sysoev (the creator of Nginx), in which he advises copying the configuration as often as possible, so as not to create bugs.
The second example is more down to earth. It happened to me in one project where a junior developer did not hesitate to reinvent the wheel instead of using obvious solutions. He left the project shortly after, leaving us with a product that was unmaintainable, which was then successfully rewritten with tried and tested solutions from tutorials.
If you look globally at the duplication, you can see that we all copy and use the code written by someone else from third-party libraries and tutorials. Should we say that copying is bad? As for me, the well-copied code can eliminate bugs and fulfill its tasks for years. But if copypasting is bad, why do most developers spend 70% of their time on Stack Overflow?
Don’t always split lengthy methods
The size of the method takes 400 lines of code? It needs to be refactored and crammed into separate functions right away! Okay, but what if the method is written so well that it looks awesome with all its 400 lines of code? Some may go for refactoring here simply because the method is too long, which means that the method logic is bloated and should be separated using the extract method.
But what if the code performs only one task? Consider one of the functions in the Go language. The size of the method is a bit more than 100 lines of code, so why hasn't it been split into several methods? The answer is obvious and simple: it's not necessary to split everything.
The coherent logic should be only in one place and can’t be divided as it happens intuitively. Often, developers understand that everything in one method should be only in the method, otherwise the code will resemble a patchwork.
Refactor large classes in all cases
We can't argue with Fowler here. In 99% of cases, a bloated class is a badly written class that doesn't correspond to any of the SOLID principles. You should refactor such a class as soon as possible to avoid issues in the future when the customer asks you to add new functionality.
Reduce the number of parameters
It happens for two reasons. First, the method does more than it should. Second, when passing parameters, the developer tries to split the entry into several parts and pass it in pieces instead of transmitting a single entity.
Such an approach violates immutability and leads to a situation where a developer does not understand how they got the result. How do you solve this issue?
First, this is what you shouldn’t do:
And this is what you should do:
Instead of passing and describing each method separately, we create a single entity for this purpose.
Mitigate divergent changes
Do you need to rewrite half of the project after changing the functionality just a little bit? Then refactoring is your way to go. Resort to methods of class extraction to put the logic into classes, so that each class performs only the actions it is responsible for. This way you get rid of duplication, and it becomes easier to extend functionality.
This issue often originates in the initial stages of the application design. A weak foundation will surely lead to this kind of staggering in the future. Issues like a poorly thought-out architecture of the application and violation of design practices result in a dysfunctional and non-viable project that can’t be maintained without causing developers outrage. To avoid this, it is worth investing more time in application design. A good foundation is the key to the success of the application — never neglect it.
Refactor code instead of endless hole plugging
If, after solving a particular task, you noticed that your solution caused multiple errors throughout the application, and this has become your daily nightmare, it’s time to refactor.
Surely, refactoring might take more time, and you'll have to inform the team that it must be done to avoid big problems in the future. But all the issues can be fixed, no matter how difficult they seem, and your peace of mind will be your greatest reward here.
Break feature envy
It's common for a function to do a bit more than it is supposed to. To fix such a mistake, you need to rewrite functions following some principles: give a function a well-structured name, ensure it's immutable (i.e. does not depend on external factors), and that it does only what it should according to its name.
As you can see, I did not specify any restrictions on the size of the function, because it is relative. As for class methods, an obvious solution to this problem is to move this method to a more appropriate place.
Handle data clumps
I personally witnessed when a weak link between the models and a poorly-structured architecture of classes caused duplications of whole models. This problem can be solved by starting from the very top level, resorting to the good class inheritance, using SOLID principles, and refactoring higher-level classes.
Avoid primitive obsession
You might have seen many USER_ADMIN_ROLE = 1 type of constants in your project. It's a bad practice to multiply such constants. Instead, use structural data types, such as objects or arrays.
This example looks better, doesn't it?
You can also use enum here:
Mind your switches
Don't use switch where you can use inheritance. Create an abstract class and de-inherit it to implement the method. It will make the code readable and divide the logic between classes.
You may ask, “Why do we have the switch operator in the first place?” The answer is not obvious. But the excessive use of if-operators in the design makes the code look ugly, and switch combines a few if-operators in itself.
Code duplication follows us everywhere. One of its manifestations is when, in order to create a new class, you have to create more classes on top of it. This is a clear violation of the Liskov SOLID principle.
How to eliminate this? The answer is to have well-structured classes and methods of these classes so that there are no duplicated code and unnecessary dependencies.
Lazy classes, speculative generalities, temporary fields — these all describe something redundant. Have you ever written a class or a method, or described the fields of a class and wondered if it is really necessary? Well, if you have, you need to think about describing this kind of behavior.
Think about the logic in a more constructive way: don't create additional classes or class fields if all the methods don't need them, and so on.
Make do with no ‘middle man’
This issue comes up when a class method refers to the methods of another class so often that it makes sense to move that method to that class rather than keep it in the current one.
Again, it all comes down to proper design and delegation of tasks that simplify work and reduce the scope for refactoring. Invest more time into application design and you probably won't need to use refactoring at all.
Fix inappropriate dependencies
This occurs when a class overuses methods and fields of another class. To solve this issue, carry the method and fields of the class to the right class. In other words, divide and delegate the execution of tasks to appropriate classes.
Refactoring is about moderation
Refactoring is not a silver bullet, it is just a form of improvement. You can improve in various ways, but everything is good in moderation. Before you start to refactor code, you should clearly understand your objectives and limits. Otherwise, you risk overloading yourself and won’t even see any code improvements since all your efforts will have been spent on creating a matrix instead.
To sum it up, overdoing refactoring is as bad as not doing it at all. Find the middle ground and allow a couple of days, hours, or story points for refactoring at the beginning of the sprint or at its end. The impact of such an incremental refactoring will pleasantly surprise you.