Software Maintainability

Code Quality

How to Refactor Duplicate Code with examples

How to Refactor Duplicate Code with examples

Amartya Jha

• 19 November 2024

To effectively refactor duplicate code, it's essential to recognize the types of duplication present and apply appropriate strategies to eliminate them. Here’s a comprehensive guide on how to identify and refactor duplicate code, complete with examples.

Types of Code Duplication

Types of Code Duplication

Types of Code Duplication

Data Duplication

Data Duplication

Data Duplication

Data Duplication occurs when the same data or constants are repeated in multiple locations within a codebase. This type of duplication is often straightforward to identify and can lead to several issues, including:

  • Inconsistency: If the duplicated data needs to be updated, forgetting to change it in every location can lead to discrepancies and bugs.

  • Maintenance Overhead: Maintaining multiple copies of the same data increases the workload for developers, particularly when changes are required.

Example of Data Duplication

Example of Data Duplication

Consider a scenario where a constant value, such as a tax rate, is hard-coded in multiple places:

def calculate_tax(amount):
    return amount * 0.07  # Tax rate is duplicated

def calculate_total(price):
    tax = calculate_tax(price)
    return price + tax

To eliminate this duplication, you can define the tax rate as a constant:

TAX_RATE = 0.07

def calculate_tax(amount):
    return amount * TAX_RATE

def calculate_total(price):
    tax = calculate_tax(price)
    return price + tax

This refactoring reduces redundancy and simplifies future updates.

Type Duplication

Type Duplication

Type Duplication arises when similar methods operate on different data types but perform essentially the same logic. This type of duplication can be more challenging to detect since it often involves different classes or structures that share similar behaviors.

Example of Type Duplication

Example of Type Duplication

Imagine two methods that process different types of user data:

public void processUser(User user) {
    // Process user
}

public void processAdmin(Admin admin) {
    // Process admin
}

These methods can be refactored using generics or interfaces to eliminate type duplication:

public void processUser(User user) {
    // Process user
}

public <T extends User> void processUserType(T user) {
    // Process either User or Admin
}

By using generics or polymorphism, you can create a single method that handles both types, reducing code duplication and enhancing maintainability.

Algorithm Duplication

Algorithm Duplication

Algorithm Duplication

Algorithm Duplication occurs when similar algorithms or logic are repeated in different parts of the codebase. This can lead to inefficiencies, as any changes to the algorithm must be replicated across all instances where it appears.

Example of Algorithm Duplication

Example of Algorithm Duplication

Consider two functions that perform similar calculations but with slight variations:

function calculateDiscountedPrice(price) {
    return price * 0.9; // 10% discount
}

function calculateSeasonalDiscountedPrice(price) {
    return price * 0.85; // 15% discount
}

You can refactor these functions into a single method that accepts parameters for flexibility:

function calculateDiscountedPrice(price, discountRate) {
    return price * (1 - discountRate);
}

const regularPrice = calculateDiscountedPrice(100, 0.1);
const seasonalPrice = calculateDiscountedPrice(100, 0.15);

This approach not only reduces duplication but also makes it easier to introduce new discount strategies without duplicating logic.

Strategies for Refactoring Duplicate Code

Strategies for Refactoring Duplicate Code

Strategies for Refactoring Duplicate Code

  1. Extract Method

  1. Extract Method

Identify repetitive code blocks and move them into a separate reusable method. This is particularly useful for shared logic across different parts of your application.

Before Refactoring:

public void calculate() {
    int sum = 0;
    for (int i = 0; i < numbers.length; i++) {
        sum += numbers[i];
    }
    System.out.println("Sum is " + sum);
}

public void displayAverage() {
    int sum = 0;
    for (int i = 0; i < numbers.length; i++) {
        sum += numbers[i];
    }
    System.out.println("Average is " + (sum / numbers.length));
}

After Refactoring:

private int calculateSum() {
    int sum = 0;
    for (int number : numbers) {
        sum += number;
    }
    return sum;
}

public void calculate() {
    System.out.println("Sum is " + calculateSum());
}

public void displayAverage() {
    System.out.println("Average is " + (calculateSum() / numbers.length));
}

This reduces redundancy and makes the code easier to update and debug.

  1. Introduce Abstraction

  1. Introduce Abstraction

Encapsulate shared functionality into a higher-level abstraction, such as a superclass, interface, or utility class.

Example: If multiple classes share common methods like startEngine or stopEngine, create a base class:

public abstract class Vehicle {
    public abstract void startEngine();
    public abstract void stopEngine();
}

public class Car extends Vehicle {
    public void startEngine() { /* ... */ }
    public void stopEngine() { /* ... */ }
}

This promotes code reuse and simplifies future extensions.

  1. Replace Conditional Logic with Polymorphism

  1. Replace Conditional Logic with Polymorphism

If duplicate logic arises from repetitive conditional statements, use polymorphism to eliminate them.

Before Refactoring:

if (shape.equals("Circle")) {
    // Circle logic
} else if (shape.equals("Rectangle")) {
    // Rectangle logic
}

After Refactoring: Define an abstract Shape class with specific implementations for Circle and Rectangle.

  1. Consolidate Duplicated Classes or Methods

  1. Consolidate Duplicated Classes or Methods

Merge similar methods or classes by parameterizing differences.

Example: Consolidate methods that vary slightly by accepting parameters or flags:

public void processUser(String userType) {
    if ("Admin".equals(userType)) {
        // Admin logic
    } else if ("Guest".equals(userType)) {
        // Guest logic
    }
}

This reduces duplication and centralizes behavior.

  1. Utilize Libraries or Frameworks

  1. Utilize Libraries or Frameworks

Leverage existing libraries to reduce custom implementations of commonly used functionality. For example, use Java’s Stream API to avoid manual iteration and summation in lists.

General Refactoring Tips

General Refactoring Tips

General Refactoring Tips

  1. Apply the Single Responsibility Principle (SRP)

  1. Apply the Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP), part of the SOLID principles of software design, states that every class or method should have one and only one reason to change. This means breaking down complex classes or methods into smaller, focused components, each handling a specific piece of functionality. Here's why and how:

  • Why SRP Matters:

    • Maintainability: Small, focused components are easier to update, debug, and test.

    • Reusability: Components designed with a single purpose can often be reused in different parts of the system.

    • Collaboration: Smaller responsibilities reduce the risk of conflicts among developers working on the same class or method.

  • How to Apply SRP:

    • Identify distinct responsibilities: For instance, in an e-commerce application, separate "Order Processing" (e.g., calculating totals) from "Logging" (e.g., saving records for audits).

    • Delegate responsibilities: Use helper classes or methods for specific tasks.

Modularize functionality: Break down large classes into smaller ones and large methods into smaller, purpose-specific methods.

Example Before Refactoring:

class Order {
    public void processOrder() {
        // Process order
        // Log order details
    }
}

Example After Refactoring:

class OrderProcessor {
    public void processOrder() {
        // Process order
    }
}

class OrderLogger {
    public void logOrderDetails() {
        // Log order details
    }
}
  1. Test Frequently

  1. Test Frequently

Testing is the backbone of successful refactoring, ensuring your changes do not introduce bugs or break existing functionality.

  • Write or Update Unit Tests Before Refactoring:

    • Use a test-first approach to define the desired behavior of the code.

    • Existing unit tests act as a safety net; refactor confidently knowing changes can be verified.

  • Benefits of Frequent Testing:

    • Immediate feedback: Catch errors as soon as they occur.

    • Reduced risk: Avoid introducing regressions while restructuring.

    • Validation of correctness: Ensure new, refactored components meet the original functionality requirements.

  • Strategies for Testing During Refactoring:

    • Baseline Tests: Run all tests before refactoring to confirm the code’s starting state is stable.

    • Incremental Testing: Test after each small change instead of waiting until the end.

    • Regression Testing: Ensure edge cases, and prior bugs fixed in the code, are still handled correctly.

  1. Use Refactoring Tools

  1. Use Refactoring Tools

Modern Integrated Development Environments (IDEs) like IntelliJ IDEA, Eclipse, or Visual Studio Code provide automated refactoring tools to make the process smoother and less error-prone.

  • Benefits of Refactoring Tools:

    • Accuracy: Tools handle renaming, extracting, and reformatting without introducing human errors.

    • Time-saving: Automation speeds up repetitive tasks.

    • Confidence: Refactoring tools usually integrate with the IDE’s error-checking, minimizing oversight.

  • Key Features in IDE Refactoring Tools:

    • Extract Method/Variable: Converts a block of code into a method or extracts a repeating expression into a reusable variable.

    • Inline Method/Variable: Merges a small method or variable directly into its callers for simplicity.

    • Rename: Safely renames classes, methods, or variables, updating all references across the codebase.

    • Move Class/Method: Reorganizes classes or methods into more appropriate locations.

    • Refactor Preview: Shows a preview of changes before applying them, allowing you to confirm intent.

Example Using Refactoring Tools: If you’re working on a long method, you can:

  1. Highlight the repetitive code block.

  2. Use “Extract Method” to create a separate, reusable function.

  3. Rename the new method to something descriptive using “Rename.”

Conclusion

Conclusion

Conclusion

  • Work Incrementally: Refactor in small, manageable chunks rather than attempting sweeping changes. This minimizes risks and makes it easier to track issues.

  • Follow Coding Standards: Refactoring is a good opportunity to clean up code formatting, naming conventions, and adhere to team guidelines.

  • Communicate with the Team: Share your refactoring goals and changes with team members, ensuring everyone understands the new structure and rationale.

By following these principles and leveraging the power of testing and refactoring tools, developers can ensure their codebase remains clean, robust, and adaptable to future needs.