The importance of refactoring
technical articlesOctober 26, 2023

The importance of refactoring

Exploring the art of code improvement

Article presentation
Examine in depth the refactoring's role in enhancing software design and maintainability.

The whats, whys, and whens of refactoring 

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”. 


tech-debt-refactoring

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:

  • Make the code more readable. The easier is to understand the code the simpler it is to integrate new functionalities and fix bugs. 
  • Reduce technical debt. Instead of pushing technical debt for later you ”fight it now” by doing simple refactorings. 
  • Understand the business better. Although not spoken about above, while refactoring you might find yourself some undocumented business cases that you didn’t know of. 
  • Reduce the support costs. By making the code easier to understand it might take you from weeks to days or even hours to fix a support ticket. 
  • Fix potential bugs. You might see yourself fixing bugs while refactoring, some bugs might stay hidden deep in your code without you even noticing until you refactor. 

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. 

Small Recap 

In this section, we’ve looked at what is refactoring, why we need it, and when to do it. Some key takeaways from this: 

  • Refactoring is the process of restructuring complex code into simpler, easier-to-understand code without changing its behavior.
  • We need refactoring to avoid technical debt and improve code quality, feature delivery time, and reduce support costs.
  • Always do refactoring, and try following the Boy Scout rule: always leave the code better than you found it. 

How do you refactor 

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: 

  • A well-known tool is SonarQube. A team could set up rules during build time to check the code coverage so that a pipeline doesn’t pass until it has X% code coverage for example 
  • Something less fancy yet very effective is your IDE. The IntelliJ IDEA has an option to run tests with coverage

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).


run with code coverage



Looking at CarParkingManager we have good code coverage. Now we can start refactoring. coverage-results

Code smells 

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: 


rename-variable-refactoring

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


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.


move-method-repository-refactoring

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. 


optional-return-refactoring

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! 


refactoring-inline-method


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:


extract-method-hour-refactoring


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. 


feature-envy-refactoring

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. 


feature-envy-after-refactoring


The next problem is that those if-else statements are hard to read, let’s replace them with a switch statement.


if-else-switch-replace-refactoring

Method is still hard to read, let’s isolate the switch statements and see where that gets us. 


isolate-swithc-statement-refactoring

Oh, another feature envy, nice, we now know how to deal with that! Let’s move that to ParkingSpot.


price-in-parking-spot-refactoring

Now if we revert that if condition and remove the redundant else after we’re left with a lot cleaner method.


invert-if-condition-refactoring

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.


duplicate-code-refactoring

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. 


replace-add-remove-refactoring

After a few more refactorings our three big methods look like this: 


result-refactoring


Key takes on refactoring

Here are a few key takes on how we do a refactoring:

  • Make sure you have good test coverage before starting. It is very important to avoid changing the code’s external behavior. You make sure of this by writing tests. 
  • Use shortcuts, typing is faster than moving your mouse around Try to learn and use the shortcuts.
  • Try renaming variables whenever they make the code hard to read. 
  • Make sure that your tests are green after each refactoring or a series of refactorings. 
  • Try splitting longer methods into smaller ones and focus on refactoring smaller parts of the code. 
  • Use the move method for a better separation of concerns. 
  • Use optionals to make the code more readable. 
  • Take baby steps, do a step-by-step refactoring process don’t jump straight into it, you’ll find out some smells show themselves only after a few refactorings have been made. 
  • Tell Don’t Ask, try avoiding asking for data and doing operations on it, move behavior to the owner of the data. This makes the code more readable as well as turns your domain model into a richer domain model 
  • Remember the Boy Scout rule, try to leave the code better than you found it.

Best practices for refactoring production code 

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. 

  • Iterate and improve. Refactoring is an iterative process. Continuously gather feedback, monitor the impact of your changes, and make improvements based on the outcomes. Iterate until you achieve the desired results. 
  • Prioritize refactoring tasks. Once you have identified the areas that require refactoring, prioritize the tasks based on their impact on code maintainability, quality, and performance. 
  • Write unit tests. Before refactoring any code, it is essential to have a comprehensive suite of unit tests in place. This ensures that the refactored code behaves as expected and prevents regressions from being introduced 
  • Plan and document. Before starting any refactoring project, it is essential to have a clear plan and document the changes you intend to make. This helps maintain focus and ensures that the refactoring process does not introduce regressions or unexpected side effects. 
  • Use version control. Utilize a version control system, such as Git, to track and manage your code changes. This allows you to easily revert to previous versions in case something goes wrong during the refactoring process. 
  • Refactor in small steps. Instead of attempting to refactor the entire codebase at once, break it down into smaller, manageable tasks. Refactoring in small steps reduces the risk of introducing bugs and makes it easier to test and validate the changes. 
  • Collaborate and communicate. If you are working in a team, it is crucial to collaborate and communicate with other developers. Discussing the refactoring process, sharing ideas, and reviewing each other’s code can lead to better outcomes and reduce the risk of introducing errors. 

Conclusion - The importance of refactoring

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.

Resources 

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.