The importance of refactoring
Exploring the art of code improvement
Exploring the art of code improvement
To understand the importance of refactoring we need to understand what refactoring is, why we need it and when we do it.
What is refactoring
Let’s answer the first question first. What is refactoring? If you do a quick google search on refactoring you quickly find this definition of it:
“In computer programming and software design, code refactoring is the process of restructuring existing computer code—changing the factoring—without changing its external behavior”
In Martin Fowler’s book “Refactoring Improving the design of existing code ” we can find another definition of it :
“Refactoring (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.”
Looking at these two definitions we can say that refactoring is the process of restructuring complex code into simpler, easier-to-understand code without changing its behavior.
Why do we need refactoring?
If we take a closer look at Martin Fowler’s definition we notice he says something about making code easier and cheaper to modify. This begs the question what does cheaper to modify mean? Well, the answer to this question is the answer to our second question Why do we need refactoring?
Let’s say you work on a project, at first, feature development is a piece of cake, you and your team would deliver the feature quite fast. Then a few months pass and you notice that it’s taking longer and longer for a feature to be delivered or a bug to be solved. Code becomes more resistant to change, you make a small change in a portion of code and you suddenly see tests failing and the application crashes. Your client tells you that it needs a new feature to be delivered fast and then, in a panic, you press the magic combination of ctrl+c and ctrl+v, you duplicate the code, make small changes to make it work and after you merge your feature you decide to ”fix it later”.
You’ve probably seen this picture or you might’ve ”felt” this over time. But what do you do when faced with this kind of context? For one you could ask the business to start a rewriting process since everything is a mess. You promise the client that this time you will use this new fancy stack or by rewriting you will surely bring value. Before bringing this forward, put yourself in the client’s shoes: Would you approve of such a lengthy and costly rewriting process? Would that bring any benefits? How long would it take until you can bring new functionalities in? Well, there could be a simpler answer to this: You refactor. You need refactoring to make the code simpler to understand and easier to change.
This answers our second question as well, you need refactoring in order to:
When do we refactor
Until now we’ve looked at what is refactoring, and why we need it. But when do we do it? The answer to this is short and simple: ALWAYS. Let’s have a look at what Robert C. Martin says in his book “Clean Code: A Handbook of Agile Software Craftsmanship”:
“The code has to be kept clean over time [...] The Boy Scouts of America has a simple rule that we can apply to our profession. Leave the campground cleaner than you found it. “
By following the Boy Scout rule we can apply simple refactoring techniques (i.e. rename variable, rename method, extract method) to keep the code clean. So whenever you find a code smell in the code you’re navigating, do not hesitate and refactor. This way we limit the technical debt.
In this section, we’ve looked at what is refactoring, why we need it, and when to do it. Some key takeaways from this:
For this section I’ve created a small refactoring exercise, one can find it on Git Hub. We’ll look over this as an example of how to approach refactoring step by step.
Until now we’ve talked about what why and when we refactor, the next obvious question is how you refactor code. For this let’s look back at the definition of refactoring:
In computer programming and software design, code refactoring is the process of restructuring existing computer code—changing the factoring—without changing its external behavior.
The last part of this definition gives us a good hint on what to do as a first step in refactoring. The last part says that we should do refactoring without changing its external behavior. How could we make sure that when refactoring we don’t break or change anything? The answer to this is also simple: Tests. Make sure you have good code coverage before jumping into a refactoring process There are tools that could help you with this:
Where do we start
Back to our example as a first step, we’ll start by writing tests for the CarPark ingManager class.
After writing our tests we’ll then use the Run With Coverage option from our IDE (in this case IntelliJ).
Looking at CarParkingManager we have good code coverage. Now we can start refactoring.
After writing tests we can now look at what code smells we can find. The simplest refactoring one can do is Rename. Badly named variables can make the code hard to read. Let’s have a look at our CarParkingManager example. Looking at the parkCar method we immediately notice that the parameters are badly named:
Looking at the diff we can see that our changes make the code a little bit more expressive.
To perform this refactoring in IntelliJ one can hit Shift+F6 or right click on the variable/method - Refactor - Rename
Let’s continue. Run the tests then commit!
Continuing on our parkCar method we could say it’s getting a bit out of control. Whenever faced with a long method try splitting it into multiple method calls and refactor separately. To do so use Extract method refactoring.
To perform this refactoring in IntelliJ one can hit Ctrl+Alt+M or right click on the lines you want to extract - Refactor - Extract Method
Now that we’ve extracted our method let’s have a look inside our findAvail ableParkingSpotId. This sounds like a repository method to me. Let’s create one in the repository interface.
The method is looking good but it can be even better! It’s getting harder and harder to carry that if-check, could there be any way to avoid carrying it everywhere? What if someone forgets to add that null check? Well in Java the answer for this is Optionals instead of returning null try returning optionals.
We’ve now ended with a method that has only a single call Let’s inline that to see what happens. After a few more refactorings the method looks like this:
To perform this refactoring in IntelliJ one can hit Ctrl+Alt+N or right-click on the variable/method - Refactor - Inline Vari able/Method.
Simple, isn’t it? Let’s run the tests and commit!
Let’s turn our eyes to the printReceipt method, this is a big one. Similar to what we’ve done before, let's find out if there are any bad names. We can see that minutes are being used as a name but hours are calculated!? Let’s fix that by renaming minutes to hours.
When doing so we've noticed that we had to change in three places, all three look the same!? Is this code duplication? Let's extract a method for the parking time calculation and our printReceipt method will look like this:
The code is still hard to read. We’ll make it better. Same steps as above, let’s see if we can improve the newly extracted method. At first glance, it looks like there is nothing we can do but we need to look deeper.
The CarParkingManager class gets a ParkingSpot and does an operation based on a value inside ParkingSpot telling ParkingSpot what to do. This is called Feature Envy. We can fix that by moving the behavior back to ParkingSpot.
To perform this refactoring in IntelliJ one can hit F6 on the method you want to move and select where you want to move it from the popup.
After moving the behavior inside our ParkingSpot model we’re left with this. Still, there are improvements to be made. We take it step by step.
The next problem is that those if-else statements are hard to read, let’s replace them with a switch statement.
Method is still hard to read, let’s isolate the switch statements and see where that gets us.
Oh, another feature envy, nice, we now know how to deal with that! Let’s move that to ParkingSpot.
Now if we revert that if condition and remove the redundant else after we’re left with a lot cleaner method.
A lot more could be done, but to keep it short we’ll stop here. You’ve probably noticed that until now we’ve repeated the same refactorings over and over and we’re left with a cleaner code. This is a very important aspect of refactoring. Try to take baby steps and not jump into a full guerilla refactoring.
At the end, we’ll look at how to deal with booleans passed as parameters. Let’s have a look at addOrRemove car. Looking at its name we notice that this method does two things: adds a car and removes a car. This breaks the Single Responsibility Principle.
In this case, we’ll have to make it work before making it worse.
Let’s extract two methods here: addCar and removeCar. Now whenever isRe move is set to true replace the call with our new remove car, similarly when it’s being set to false call the addCarMethod.
After a few more refactorings our three big methods look like this:
Here are a few key takes on how we do a refactoring:
Analyze the codebase: Begin by analyzing your codebase to identify areas that require refactoring. Look for signs of code duplication, complex code, and performance bottlenecks.
In this article, we've delved into the transformative power of refactoring, focusing on its importance in ensuring software remains agile, clean, and easily understandable. By adhering to refactoring best practices, we, the developers, can navigate complex projects with ease, ensuring they deliver efficient, high-quality software. As technology continues to evolve, it's crucial for us to embrace these techniques, ensuring the longevity and adaptability of our work.
Practice makes perfect, try solving katas yourself to get yourself going with refactoring.
Some time ago I was introduced to the refactoring/Craftsmanship world in a workshop hosted by Victor Rentea. You could learn subjects regarding refactoring and more by joining the European software crafters community.