Code Refactoring in Java: Tips, Best Practices and Methods

What is Java code refactoring?

Java code refactoring is a type of code optimization that is performed without affecting the external behavior of the code. It improves the structure and quality of existing code through incremental and small-scale optimizations. The goal of refactoring is to improve code readability, performance, maintainability, efficiency, etc.

Martin Fowler is a leading authority in this field and a very prolific writer. He has explored the topic of code design and refactoring in many articles and books. In his work “Refactoring: Improving the Design of Existing Code,” he explains the essence of refactoring brilliantly:

“Refactoring is the process of making modifications to the code to improve the internal structure of the program without changing the external behavior of the code. Refactoring is a methodical method of program organization that has been developed over time to maximize Minimize the chance of errors introduced during the sorting process. The core is to continuously carry out some small optimizations. Each optimization may seem inconspicuous, but the cumulative effect is significant.”

–Martin Fowler

When refactoring Java code, consider the following common optimization measures:

  • Rename variables, classes, functions, and other elements to make them more readable and self-descriptive.
  • Reduce method or function calls by inlining and use more concise content.
  • Extract chunks of code within functions and move them into new stand-alone functions to enhance modularity and readability.
  • Eliminate redundancy by removing multiple code snippets for the same functionality and merging them into one.
  • Split a class or module that handles too many responsibilities into smaller, more cohesive components.
  • Merge classes or modules with similar functions to simplify the structure.
  • Optimize code performance.

Java code refactoring skills

Renaming of variables and methods

Choosing representative names for variables and methods is an important way to enhance the readability of your code.

Code readability is one of the key elements in building a high-quality code base. Readable code clearly expresses its purpose, while hard-to-understand code increases the risk of errors during refactoring. Using meaningful variable and method names reduces the need for comments and reduces communication costs.

// Before reconstruction
int d = 30; // Number of days
int h = 24; // hours of day

// After reconstruction
int daysInMonth = 30;
int hoursInDay = 24;

Extraction of methods

In Java code refactoring technology, method extraction is a common and practical strategy. When a method becomes too long and complex, extracting some functionality into a new method can make the original method more concise and readable. This not only makes the code more maintainable, but also increases its reusability.

Let’s say you have a simple class that processes orders and calculates subtotals, taxes, and total charges.

public class OrderProcessor {
    private List<Item> items;
    private double taxRate;

    public double processOrder() {
        double subtotal = 0;
        for (Item item : items) {
            subtotal + = item.getPrice();
        }
        
        double totalTax = subtotal * taxRate;
        double totalCost = subtotal + totalTax;
        
        return totalCost;
    }
}

You can refactor this code to extract the code for calculating subtotals, taxes, and total costs into three separate methods: calculateSubtotal, calculateTax, and calculateTotalCost, making the class more readable, modular, and reusable.

public class OrderProcessor {
    private List<Item> items;
    private double taxRate;

    public double processOrder() {
        double subtotal = calculateSubtotal();
        double totalTax = calculateTax(subtotal);
        return calculateTotalCost(subtotal, totalTax);
    }
    
    private double calculateSubtotal() {
        double subtotal = 0;
        for (Item item : items) {
            subtotal + = item.getPrice();
        }
        return subtotal;
    }
    
    private double calculateTax(double subtotal) {
        return subtotal * taxRate;
    }
    
    private double calculateTotalCost(double subtotal, double totalTax) {
        return subtotal + totalTax;
    }
}

Eliminate “magic” numbers and strings

“Magic” numbers and strings are values that are hardcoded directly into the code. This practice not only reduces the maintainability of the code, but may also lead to inconsistent results and an increase in errors due to input errors. To avoid such problems, you should avoid using hard-coded values and instead refactor your code by using clearly descriptive constants.

// Before reconstruction
if (status == 1) {
    // ... code for active state ...
}

// After reconstruction
public static final int ACTIVE_STATUS = 1;

if (status == ACTIVE_STATUS) {
    // ... code for active state ...
}

Code Reuse

Code reuse refers to removing duplicate or similar code segments that appear in multiple places in a code base. Such code not only reduces code quality and efficiency, but may also cause bugs to appear more frequently and the code base to become more complex. Therefore, developers often feel offended by this type of code. In order to optimize the code, we can consider extracting repeated parts to create reusable methods or functions, while ensuring that the functionality and logic of the original code are maintained during the refactoring process.

Before refactoring
public class NumberProcessor {

    // Calculate the sum
    public int calculateTotal(int[] numbers) {
        int total = 0;
        for (int i = 0; i < numbers.length; i + + ) {
            total + = numbers[i];
        }
        return total;
    }

    // Calculate the average
    public double calculateAverage(int[] numbers) {
        int total = 0;
        for (int i = 0; i < numbers.length; i + + ) {
            total + = numbers[i];
        }
        double average = (double) total / numbers.length;
        return average;
    }
}
After reconstruction
public class NumberProcessor {

     // Calculate the sum
    public int calculateSum(int[] numbers) {
        int total = 0;
        for (int i = 0; i < numbers.length; i + + ) {
            total + = numbers[i];
        }
        return total;
    }

    // Calculate the sum
    public int calculateTotal(int[] numbers) {
        return calculateSum(numbers);
    }

    // Calculate the average
    public double calculateAverage(int[] numbers) {
        int total = calculateSum(numbers);
        double average = (double) total / numbers.length;
        return average;
    }
}

In the optimized code, we extracted the logic for calculating the sum of the arrays into a separate method called calculateSum. The calculateTotal and calculateAverage methods can now directly call calculateSum to get the sum of an array, thus avoiding code duplication.

Simplified method

Over time, as more people maintain the code, the code base tends to become stale and cluttered. In order to ensure the clarity and maintainability of the code, it is very necessary to refactor the code to make it easier to understand, maintain and expand.

In the process of simplifying methods, start by identifying those methods that contain complex nested logic and take on too many responsibilities. Then, you can simplify them by following these steps:

  • Follow the Single Responsibility Principle (SRP) to align the functional division of your approach.
  • Extract some functions and create new sub-methods.
  • Remove useless and redundant code.
  • Reduce the nesting level within methods to make their structure clearer.

Next, we will use a Java code refactoring example to specifically demonstrate how to simplify the method.

Before simplification
public class ShoppingCart {
    private List<Item> items;

    // Calculate total price
    public double calculateTotalCost() {
        double total = 0;
        for (Item item : items) {
            if (item.isDiscounted()) {
                total + = item.getPrice() * 0.8;
            } else {
                total + = item.getPrice();
            }
        }
        if (total > 100) {
            total -= 10;
        }
        return total;
    }
}

We can make the above example more concise by extracting the calculateItemPrice logic into the calculateItemPrice and applyDiscount methods and using the ternary operator to simplify the conditional judgment.

Simplified
public class ShoppingCart {
    private List<Item> items;

    // Calculate total price
    public double calculateTotalCost() {
        double total = 0;
        for (Item item : items) {
            total + = calculateItemPrice(item);
        }
        total -= applyDiscount(total);
        return total;
    }

    // Calculate the price
    private double calculateItemPrice(Item item) {
        return item.isDiscounted() ? item.getPrice() * 0.8 : item.getPrice();
    }

    // Get full discount
    private double applyDiscount(double total) {
        return total > 100 ? 10 : 0;
    }
}

Red and green reconstruction process

Image source: Codecademy

Image source: Codecademy

Red-green refactoring, also known as test-driven development (TDD), is a code refactoring technique that emphasizes writing tests first and then writing code that can pass these tests. The technique is an iterative process, with each iteration consisting of writing new tests and enough code to pass those tests, and finally refactoring the code.

This technology consists of three stages:

  • Red Phase: In this phase, you haven’t written the actual code yet. You first need to write a set of tests (marked in red) that are expected to fail because there is no corresponding implementation to satisfy these test conditions.
  • Green Phase: The purpose of this phase is to write enough code to pass the previously written failed tests, i.e. to make the tests green. Note that the goal at this point is not to write perfect or highly optimized code, but simply to ensure that the tests pass.
  • Refactoring phase: After confirming that the code has successfully passed all tests, you should focus on refactoring the code at this time to improve its performance and structure without changing its basic functions to ensure that the tests can still pass smoothly. .

After each test case is completed, you will enter the next cycle and continue to write new test cases and corresponding code, and then refactor the code to achieve better optimization.

Optimize code that violates the single responsibility principle

Maybe you already know something about the SOLID principles in object-oriented programming. SOLID is an acronym for Five Design Principles.

  • S: Single Responsibility Principle. This principle emphasizes that a class should have only one reason for change. In short, a class should only be responsible for one function point.
  • O: Open Closed Principle, software entities should be extensible rather than modifiable.
  • L: Liskov Substitution Principle, an object should be able to be replaced by its subtype without affecting the correctness of the program.
  • I: Interface Segregation Principle, clients should not be forced to rely on interfaces they do not use.
  • D: Dependency Inversion Principle, high-level modules should not depend on low-level modules, both should rely on abstraction.

The single responsibility principle is the first principle. This principle emphasizes that each class should have only one reason for change, that is, a class is only responsible for one function point. Adhering to the single responsibility principle is one of the basic ways to ensure that the code is maintainable, readable, flexible and modular. one. Below we will show an example of the OrderProcessor class, which violates the single responsibility principle because it takes on both order processing and information logging responsibilities.

Before refactoring
public class OrderProcessor {

    // Process the order
    public void processOrder(Order order) {
        //Order verification

        
        // Processing logic
        
        // logging
        Logger logger = new Logger();
        logger.log("Order processed: " + order.getId());
    }
}

In order to follow the single responsibility principle, we can refactor this class into three classes: one is the OrderProcessor class, which is only responsible for order processing; the OrderValidator is responsible for order verification, and the other is the OrderLogger class, which is specifically responsible for logging.

After reconstruction
public class OrderProcessor {
    
    private final OrderValidator orderValidator;
    
    public OrderProcessor(OrderValidator orderValidator) {
        this.orderValidator = orderValidator;
    }
    
    // Process the order
    public void processOrder(Order order) {
        //Order verification
        if(!orderValidator.validate(order)) {
            throw new IllegalArgumentException("Order is not valid");
        }
        
        // Processing logic
        // ...

        // logging
        OrderLogger logger = new OrderLogger();
        logger.logOrderProcessed(order);
    }
}

public class OrderValidator {

    //Order verification
    public boolean validate(Order order) {
        //Verification logic
        // ...
        return true;
    }
}


public class OrderLogger {

    
    public void logOrderProcessed(Order order) {
        Logger logger = new Logger();
        logger.log("Order processed: " + order.getId());
    }
}

14 Best Practices for Java Code Refactoring

Code refactoring is an important step that can significantly improve code quality and brings many of the benefits we highlighted earlier. But you also need to be careful when refactoring, especially when dealing with a large or unfamiliar code base, to avoid inadvertently changing the functionality of the software or creating unforeseen problems.

To avoid any potential issues, here are some tips and best practices for refactoring your Java code:

  1. Keep functionality the same: The primary goal of Java code refactoring is to improve code quality. However, the program’s external behavior, such as how it responds to input and output and other user interactions, should remain unchanged.
  2. Understand the code well: Before you start refactoring a piece of code, make sure you fully understand the code you are about to refactor. This includes its functionality, interactions and dependencies. This understanding will guide your decisions and help you avoid making changes that may affect the functionality of your code.
  3. Break the Java refactoring process into small steps: Refactoring large software, especially if you don’t know enough about it, can be overwhelming. However, by breaking the refactoring process into smaller, manageable steps, you can make the workload lighter, reduce the risk of errors, and make it easier to continually verify your changes.
  4. Create backups using version control: Since refactoring involves making changes to the code base, there is a chance that things may not go as planned. It’s a good idea to back up your working software on a separate branch to avoid wasting a lot of time and resources when you can’t figure out which changes broke your software. Version control systems like Git allow you to manage different software versions simultaneously.
  5. Test your changes frequently: The last thing you want to do when refactoring your code is to accidentally break the functionality of your program or introduce bugs that affect your software. Before making any major code changes, especially refactoring, building a test suite is a great way to provide a safety net. These tests verify that your code behaves as expected and that existing functionality remains intact.
  6. Leverage refactoring tools: With modern IDEs like Eclipse and IntelliJ, Java code refactoring doesn’t have to be an intense process. For example, IntelliJ IDEA includes a powerful set of refactoring features. Some features include safe deletion, extraction of methods/parameters/fields/variables, inline refactoring, copy/move, etc. These features make your job easier and reduce your chances of introducing errors during refactoring.
  7. In-depth understanding of code changes: During the refactoring process, it is very important to have an in-depth understanding of the code changes you have made. It can help you quickly identify and solve possible problems.
  8. Get the most out of unit tests: As developers, we need to make sure that when we refactor our code, we don’t break existing applications or introduce new bugs. A complete unit test suite can help you detect regression issues and ensure functional integrity, while also facilitating teamwork and long-term code maintenance.
  9. Continuously track performance changes and provide feedback: Java code refactoring is not just about improving the code structure, it also involves performance optimization. By continuously monitoring performance metrics, you can ensure that your refactoring efforts are making substantial progress.
  10. Conduct a peer code review: After the refactoring is completed, it is recommended to invite another developer to review your changes. They can discover problems that may have been overlooked by you from a new perspective and provide valuable feedback of.
  11. Recording refactoring changes: When you work with other developers, it’s important to document your refactoring changes because it improves transparency and collaboration, and also helps new hires learn more quickly. project.
  12. Perform regression testing: After completing the refactoring, regression testing is required to ensure that all existing functionality is retained and that the newly introduced logic does not conflict with existing code.
  13. Keep the team in sync: When working in a multi-person development team, it is important to communicate your refactoring changes in a timely manner to avoid conflicts and help the team better adapt to new changes.
  14. Perform rollback when necessary: If you encounter unsolvable problems during the refactoring process, do not hesitate to roll back to a stable state immediately to avoid wasting more time and resources.

Conclusion

Refactoring is a vital technical practice that is key to ensuring the long-term success of software projects. By incorporating the techniques we discussed earlier into your development cycle and strictly following best practices, you can transform any complex and messy code base into a readable, maintainable, and scalable software solution. But please note that Java code refactoring is not a one-time task, but a process that can be continuously integrated into your development cycle.

FAQ

When should I refactor my Java code?

Code refactoring can be done at any stage of software development. Whether you’re adding new features, fixing bugs, or optimizing a hard-to-understand piece of code, it’s a good time to refactor. Regularly setting aside time for refactoring can avoid the accumulation of technical debt and maintain the high quality of the code base.

How do you decide which code needs to be refactored?

You can start by identifying areas in your code base that are difficult to understand, have repetitive logic, or are prone to bugs. Look for code with lengthy methods, complex conditionals, and try to follow the “single responsibility principle” to improve code organization.

How to ensure that refactoring does not introduce bugs?

Having a sound suite of automated tests is key to reducing the risk of introducing bugs during refactoring. Before starting to refactor, make sure your code has good test coverage. This will help you catch any possible regression issues and ensure the functional stability of the code.

How to refactor a class in Java?

First, you need to identify the target class and gain a deep understanding of its behavior and dependencies. You can then consider splitting bulky methods and moving methods into more suitable classes, while leveraging inheritance and interfaces to achieve a clearer structure. In addition, renaming variables and methods, reorganizing code structure, and simplifying conditional statements can improve code readability. Finally, be sure to test all changes thoroughly to ensure functional integrity.

Finally, recommend a low-code development tool

Jnpf is a platform for rapid application development. It is developed based on Java/.Net and focuses on low-code development. It aims to provide visual interface design and logic arrangement, greatly reducing the development threshold. It is pre-configured with a large number of out-of-the-box functions and can be customized and assembled flexibly as needed. Stable and powerful integration capabilities, one-time design, complete multi-terminal adaptation. Jnpf provides a user-friendly open interface that can be easily integrated with various build tools and IDEs. It also supports plug-ins and custom rules, allowing developers to customize it according to the specific needs and standards of the project. More details can be found in the jnpf official documentation.

Through it, IT professionals who are weak in coding can also build personalized management applications by themselves, lowering the technical threshold. Developers can develop various application management systems with only a small amount of code or no code. Since most of them are developed using components and encapsulated interfaces, development efficiency is greatly improved.

If you are also interested in using JNPF, you can quickly try it through http://www.jnpfsoft.com/?csdn.