Published in:
Uncategorised
Refactoring the code with no pain
Refactoring the code in legacy software is an opportunity to restructure and improve the code source while keeping the same features. Mastering refactoring techniques can save time than throwing the code away and building it from scratch. Refactoring of code becomes strategic when you need to perform modifications to a legacy code base, especially when it lacks unit tests to ensure you won’t introduce regressions. This post is a short journey toward 8 refactoring techniques that would ease your work as a developer.
They stem from a live session on refactoring we recently hosted. Check out this page to register to the next sessions.
Refactoring Tip #1: Give more meaningful names to code elements
Let’s start with a simple refactoring technique, the renaming. A common guideline in software engineering is to name your variables, functions, and classes so that we can understand their intent and their business value. Good naming will drastically improve the understandability of a piece of code. Assume now you cope with the following code in a class called TennisGame:
Not an easy one, right? If you struggle a little bit, you’ll eventually figure out the s variable, which has a key role in this function, is used to store the displayed score of the tennis game. Again, with your IDE, a quick win is to perform a rename operation:
At a glance, we quickly understand now the intent of this variable.
Refactoring Tip #2: Extract loop content in a separate method
Consider the following code (from the Gilded Rose kata):
This code has several indentation levels, making it more difficult to read and understand. One quick operation supported by all main IDEs is the Extract Method feature. Give a meaningful name, and here you go!
We now have two methods with separate responsibilities (browse the collection and compute each item) and a removed indentation level. Extract method operations are common when refactoring the code.
Refactoring Tip #3: Separate boolean conditions in nested if
This one is not necessarily valid in all contexts, but in some cases, it can help make the code more readable and help understand the code’s decision tree. You should see this practice as an intermediate step during your refactoring. Consider the following code:
If we burst the conditions in the if statement, we obtain this result:
See the intent? You can observe that code in … else branch is indeed duplicated, but you may agree that the conditions are easier to read now. Once you’re more confident with this code, for instance, if you manage to write unit tests that cover the different cases, you’ll be able to make new changes in your code.
Refactoring Tip #4: Rewrite negative conditions
Similar to Tip #3, this is an intermediate step. Too many conditions can increase the mental load required to understand a code. A piece of advice can be to remove the negations, which means turning the following code:
Into this one:
Even though the first condition has no instruction, it still improves reading this code.
Refactoring Tip #5: Encapsulate a static dependency
This case commonly happens with the usage of the Singleton pattern. There are 2 issues with a singleton: 1) you can’t override it 2) its shared state can change from different unit tests execution, making them not reproducible depending on their execution order. The idea here is to extract the call to the dependency in a method so that we can override the default behavior in a sub-class during our unit tests. As an example here:
This can be turned into the following:
The getLoggedUser method will then be overridden if needed in our tests suite.
Refactoring Tip #6: Return early to reduce complexity
A method containing multiple if conditions and exits (return or exceptions thrown) might have a complex decision tree with several indentation levels. The suggestion for refactoring the code here is to turn conditions into their negative form to leave the method as soon as possible. Think as a Fail fast approach. Here is below an illustration with a method containing four exits points:
We can turn it into the following code that contains four flattened if conditions, reducing the cognitive complexity of understanding this code:
You might see this is counter-intuitive if we consider Tip #4. Remember that the most important thing is to remain pragmatic, depending on the context of your code. Refactoring is about improving the maintainability of the source code, so you’re likely to go through different intermediate steps; trust yourself to choose the one that helps you the most.
Refactoring Tip #7: Declare variables at the closest locations of their usage
An easy trick to improve the readability of a source code is to make sure you don’t declare a variable and use it several lines after. Reading code that doesn’t follow this practice lets us think you give irrelevant information, then do something else, and finally, use the previous information since you need it now. In this example, there’s no need to declare conversionRate so early:
So while I’m reading the first condition, I’m confused about the role of this variable. Instead, declare it just before using it:
The full story is easier to read, and you can keep refactoring the code.
Refactoring Tip #8: Encapsulate primitives types into business types
Business types make it easier to express intention and logic while ensuring validation. In the following code, we need to make assumptions about what are the three input parameters (despite the clear naming):
But you can see that the business logic with the amount < 0 condition is scattered in the code and should not be there. Instead, we can suggest the following code, introducing the Amount and Currency classes, which allows encapsulating the previous condition into a method isNegative().