Home / Blog /
6 code refactoring techniques and how to choose
//

6 code refactoring techniques and how to choose

//
Tabnine Team /
7 minutes /
April 1, 2024

What are code refactoring techniques? 

Code refactoring refers to strategic changes made to software code to make it more efficient and maintainable, but without altering its external behavior. 

The primary aim of code refactoring is to improve the non-functional attributes of the software system. Often, this is done to rectify ‘code smells’—parts of the code that do not follow good coding practices. Code refactoring helps fight technical debt, transforming messy, inefficient code into a clean and simple design, and making code easier to maintain and extend.

There are multiple code refactoring techniques, including red-green refactoring, the extract method, and the simplifying method. A key aspect of all these techniques is that they do not add new functionality. Instead, they improve the quality of the existing code, making it easier to work on in the future.

Essential code refactoring techniques with examples 

1. Red-green refactoring

Red-green refactoring is a technique that is commonly used in test-driven development (TDD). It involves three basic steps: Red, where you write a test that fails; Green, where you write code to make the test pass; and Refactor, where you improve the code while ensuring that the tests still pass.

The purpose of red-green refactoring is to provide rapid feedback. You will know immediately if your changes have broken the functionality of your code. This technique encourages simplicity and requires that you think critically about your design.

Code example

Red stage:

@Test

public void testSquareArea() {

    int result = Calculator.squareArea(4);

    assertEquals(20, result);  // This will fail

}

In this stage, a test testSquareArea is written to test the squareArea method of the Calculator class. This test is expected to fail initially as the squareArea method has not been implemented yet. The test checks if the method returns a value of 20 when the input is 4, which is incorrect since the area should be 16 (4*4).

Green stage:

public int squareArea(int side) {

    return side * side;  // This will make the test pass

}

 

Now, the squareArea method is implemented in a way that will make the test pass. It simply multiplies the side length by itself to calculate the square’s area, which is the correct logic.

Refactor stage:

public int squareArea(int side) {

  return side  * side;

}

In this stage, the code is refactored to improve its readability and maintainability without changing its behavior. A local variable area is introduced to hold the result before it is returned, which could make the code easier to understand and debug.

2. Extract method

The extract method is one of the most common refactoring techniques. It involves creating a new method by extracting a group of code lines from an existing method. This approach makes code more readable and reusable, reduces complexity, and helps isolate independent parts of the code.

This technique is particularly useful when you have a method that has become too complicated or long. By breaking it down into smaller, more manageable methods, you can increase the code’s maintainability and readability.

Code example

Before:

public void displayUserInfo() {

    System.out.println("Username: " + username);

    System.out.println("Birthdate: " + birthdate);

    System.out.println("Age: " + age);

}

 

Initially, the displayUserInfomethod contains all the logic for displaying the user’s information.

public void displayUserInfo() {

    displayUsername();

    displayBirthdate();

    displayAge();

}




private void displayUsername() {

    System.out.println("Username: " + username);

}




private void displayBirthdate() {

    System.out.println("Birthdate: " + birthdate);

}




private void displayAge() {

    System.out.println("Age: " + age);

}

 

The method is refactored using the extract method technique. The logic for displaying each piece of user information is moved into its own separate method (displayUsername, displayBirthdate, displayAge). This makes the displayUserInfo method more readable and the code more organized.

A further refactoring could create a single method and use an argument to display the relevant information:

 

public void displayInformation(String infoType) {
  if( infoType == “username” ) {
    System.out.println(“Username: “ + username);

  else if( infoType == “birthdate” ) {
    System.out.println(“Birthdate: “ + birthdate );
  }
   
 else {
    System.out.println(“Age: “ + age );
}

 

  1. Simplifying method

The simplifying method might involve replacing a complex conditional with query, replacing a parameter with explicit methods, and so on. The goal is to make the code more understandable and manageable.

The simplifying method is crucial in maintaining clean code. It reduces confusion, decreases the chance of errors, and makes it easier to test. When your methods are simple, they become more transparent and predictable, leading to a more robust codebase.

Code example

Before:

public double calculateDiscount(double price) {

    if (price > 1000) {

        return price * 0.1;

    } else {

        return price * 0.05;

    }

}




The calculateDiscount method uses an if-else statement to determine the discount rate based on the price.




public double calculateDiscount(double price) {

    return price * ( (price > 1000) ? 0.1 : 0.05);

}

 

The method is refactored to use a ternary operator instead, which simplifies the code and reduces the number of lines. The logic remains the same, but it’s now more concise.

4. Composing method

The composing method helps maintain code at a high level of abstraction. It involves breaking down code into smaller, more manageable parts, each of which accomplishes one task.

This approach makes your code more readable and maintainable. It allows developers to understand the code more quickly and reduces the chance of bugs. The composing method also makes the code more reusable, as each method can be used independently of the others.

Code example

Before:

public void saveUserDetails() {

    // Validate details

    // Save details to database

    // Notify user

}

 

The saveUserDetails method contains all the logic for validating details, saving them to the database, and notifying the user.

 

public void saveUserDetails() {

    validateDetails();

    saveToDatabase();

    notifyUser();

}




private void validateDetails() { /*...*/ }




private void saveToDatabase() { /*...*/ }




private void notifyUser() { /*...*/ }

 

The method is refactored using the composing methods technique. The logic for each task is moved into its own separate method (validateDetails, saveToDatabase, notifyUser), making the saveUserDetails method more readable and the code more organized.

5. Abstraction

Abstraction in code refactoring involves recognizing and encapsulating common behaviors or states within your code into separate entities, such as methods or classes. By doing so, you create a higher-level representation, which hides the complex, lower-level details. This promotes code reuse, reduces redundancy, and improves maintainability. 

Abstraction can be achieved in several ways, for example, creating abstract classes, interfaces, or encapsulating specific behaviors within methods. This technique is fundamental in object-oriented programming and is crucial for managing complexity in large codebases.

Code example

Before:

 

public class Car {

    public void startEngine() {

        // Engine start logic

    }




    public void stopEngine() {

        // Engine stop logic

    }




    public void accelerate() {

        // Accelerate logic

    }




    public void brake() {

        // Brake logic

    }

}

 

The Carclass contains methods for starting/stopping the engine, accelerating, and braking.

 

After:

public abstract class Vehicle {

    public abstract void startEngine();

    public abstract void stopEngine();

    public abstract void accelerate();

    public abstract void brake();

}




public class Car extends Vehicle {

    @Override

    public void startEngine() {

        // Engine start logic

    }




    @Override

    public void stopEngine() {

        // Engine stop logic

    }




    @Override

    public void accelerate() {

        // Accelerate logic

    }




    @Override

    public void brake() {

        // Brake logic

    }

}

In this refactored example, an abstract Vehicle class is created to encapsulate the common behaviors of different vehicles. The Car class then extends Vehicle and implements the abstract methods. This abstraction allows for the addition of other vehicle types in the future with ease, promoting code reuse and maintainability.

6. Preparatory refactoring

Preparatory refactoring is the process of making a codebase easier to work with before implementing new features or making bug fixes. This technique helps to prevent technical debt and makes the addition of new functionality smoother and less error-prone.

Preparatory refactoring can involve a variety of tasks, such as simplifying complex methods, removing duplicate code, or improving the names of variables and methods. The goal is to prepare the codebase for the changes to come, making the development process more efficient.


How to choose code refactoring techniques

Here are a few considerations that can help you choose the best code refactoring techniques for your project.

Code complexity

When dealing with high code complexity, choosing refactoring techniques that simplify and clarify the code is crucial. Complex code often leads to increased maintenance costs and higher risks of bugs. 

Techniques like the simplifying method or composing method are particularly effective in these cases. They help break down complex code into more manageable, readable parts, making it easier for developers to understand and modify the codebase.

Redundant or duplicate code

Redundant or duplicate code can significantly hamper the efficiency and readability of a software system. When encountering such issues, techniques like abstraction and extract method are highly beneficial. 

Abstraction allows you to encapsulate common behaviors or states, reducing redundancy and facilitating code reuse. The extract method helps in isolating repeated code into separate methods, enhancing the modularity and maintainability of the code.

Poorly organized data structures

For poorly organized data structures, refactoring techniques that focus on improving data encapsulation and abstraction are vital. This often involves reorganizing classes, methods, and data into a more logical and cohesive structure. 

Techniques such as abstraction can be instrumental in this process, helping to create a more intuitive and efficient data model. Additionally, applying the composing method can further enhance the organization by breaking down complex data manipulations into simpler, more focused operations.

Complex conditional logic

When facing complex conditional logic, refactoring techniques like the simplifying method can be extremely useful. This technique involves replacing complicated conditional statements with simpler, more concise expressions, such as using ternary operators or query methods. The goal is to reduce the complexity of the conditions, making them easier to read and maintain. 

Inappropriate use of classes and methods

In scenarios where classes and methods are used inappropriately, refactoring techniques focused on proper encapsulation and abstraction should be employed. This might involve restructuring the code to ensure that each class and method has a clear, single responsibility. 

Techniques like abstraction and extract method are particularly useful in such cases. They promote better organization of the codebase, ensuring that each component is well-defined and serves a specific purpose. Correcting the inappropriate use of classes and methods enhances code modularity, making it easier to extend, maintain, and reuse.


Automating code refactoring with generative AI

Given the challenges of code refactoring, recent advances in generative AI can be a big help to development teams. Tabnine is an AI-powered coding assistant that can predict and generate code completions in real time, and can provide automated code refactoring suggestions, which are sensitive to the context of your software project.

Tabnine integrates with your Integrated Development Environment (IDE). As you type in your IDE, Tabnine analyzes the code and comments, predicting the most likely next steps and offering them as suggestions for you to accept or reject.

Here are a few examples showing how Tabnine can be used to perform code refactoring:

Tabnine utilizes a Large Language Model (LLM) trained on reputable open source code with permissive licenses, StackOverflow Q&A, and even your entire codebase (Enterprise feature). This means it generates more relevant, higher quality, more secure code than other tools on the market. 

Tabnine has recently released Tabnine Chat (currently available on Beta) which is an AI assistant trained on your entire codebase, safe open source code, and every StackOverflow Q&A, while ensuring all of your intellectual property remains protected and private. Tabnine Chat integrates with your IDE and can help you generate comprehensive and accurate code documentation with low effort. 

Get a 90-day free trial of Tabnine Pro today, or talk to a product expert.