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.
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:
Let’s move on to the cons:
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.
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.
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?
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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 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.