Index

Refactoring to Design

Java Object-Oriented Design

15.1 Code Smells and Refactoring

In software development, maintaining clean, modular, and extensible code is essential to long-term success. Over time, however, codebases can deteriorate due to rushed development, unclear requirements, or lack of design awareness. These issues often manifest as code smells—symptoms in code that may not be outright bugs but indicate deeper structural problems. Recognizing and resolving these smells through refactoring is a vital part of sustaining high-quality object-oriented design.

What Are Code Smells?

A code smell is a surface-level indication that something may be wrong in the system's design. While a smell doesn’t always mean there’s a bug, it suggests the presence of technical debt or future maintainability issues. The term was popularized by Martin Fowler in his seminal book Refactoring: Improving the Design of Existing Code, where he emphasizes that smells should trigger deeper inspection.

Smells are not absolute—they depend on context, scale, and the domain—but being able to identify them is a critical skill for any Java developer.

Common Object-Oriented Code Smells

Below are some frequent smells particularly relevant to Java and object-oriented programming:

  1. Long Method Methods that try to do too much tend to become unreadable and hard to maintain. Smell indicator: A method spans dozens of lines and has multiple responsibilities.

  2. Large Class Classes bloated with fields and methods may indicate poor separation of concerns. Smell indicator: The class has too many responsibilities or manages unrelated data.

  3. Duplicated Code Copy-pasted code across methods or classes leads to inconsistencies and makes maintenance harder. Smell indicator: Similar blocks of code appear in multiple places.

  4. Feature Envy A method that frequently accesses the internals of another class likely belongs there. Smell indicator: Calls to another object’s getters dominate the method.

  5. Data Clumps Groups of parameters or fields that tend to appear together suggest the need for a new object. Smell indicator: Methods regularly pass the same three or four arguments.

  6. Switch Statements or Long Conditionals Repeated conditional logic suggests polymorphism might better serve the design. Smell indicator: switch or if-else blocks that dispatch behavior based on type.

The Role of Refactoring

Refactoring is the process of improving the internal structure of code without changing its observable behavior. It’s a disciplined way to clean code while retaining all of its functionality. Refactoring is not about adding features; it’s about improving design and maintainability.

Refactoring helps achieve:

It’s crucial to refactor with the support of a good suite of unit tests to ensure that changes don’t break existing functionality.

Example: Refactoring a Long Method

Before Refactoring:

public void printInvoice(Order order) {
    System.out.println("Invoice for Order #" + order.getId());
    for (Item item : order.getItems()) {
        double price = item.getPrice() * item.getQuantity();
        System.out.println(item.getName() + ": " + price);
    }
    System.out.println("Total: " + order.getTotal());
    System.out.println("Thank you for shopping!");
}

Smell: This method mixes multiple responsibilities—printing headers, calculating prices, and formatting output.

After Refactoring (using Extract Method):

public void printInvoice(Order order) {
    printHeader(order);
    printItems(order);
    printFooter(order);
}

private void printHeader(Order order) {
    System.out.println("Invoice for Order #" + order.getId());
}

private void printItems(Order order) {
    for (Item item : order.getItems()) {
        double price = item.getPrice() * item.getQuantity();
        System.out.println(item.getName() + ": " + price);
    }
}

private void printFooter(Order order) {
    System.out.println("Total: " + order.getTotal());
    System.out.println("Thank you for shopping!");
}

Result: The main method now reads clearly, and each step can be reused, tested, or modified independently.

When and How to Refactor

You should refactor when:

Use IDE features (like in IntelliJ IDEA or Eclipse) to assist with safe refactoring. Always validate your changes with tests.

Conclusion

Code smells are a helpful way to detect areas where your code might be violating design principles such as the Single Responsibility Principle or DRY (Don't Repeat Yourself). Through disciplined refactoring, developers can transform smelly code into clean, modular, and robust systems. Rather than treating refactoring as a chore, embrace it as a key tool for evolving your design, especially in growing or long-lived Java projects.

Index

15.2 Extract Method, Class, and Interface

Refactoring Techniques: Extract Method, Class, and Interface

In object-oriented software development, refactoring is essential to keep code maintainable, readable, and extensible. Among the most powerful and frequently used refactoring techniques are Extract Method, Extract Class, and Extract Interface. These techniques help developers simplify complex code, improve cohesion, and reduce coupling — all of which contribute to cleaner design and better software architecture.

Let’s explore each of these techniques with practical examples and best practices.

Extract Method: Breaking Down Complexity

The Extract Method refactoring technique involves moving a block of code from a larger method into a new, well-named method. This improves readability, reusability, and testability.

Before Refactoring

public class InvoicePrinter {
    public void printInvoice(Order order) {
        System.out.println("Invoice for Order #" + order.getId());
        for (Item item : order.getItems()) {
            double price = item.getPrice() * item.getQuantity();
            System.out.println(item.getName() + ": " + price);
        }
        System.out.println("Total: " + order.getTotal());
    }
}

This method is doing too much—printing headers, iterating items, computing prices, and printing totals.

After Refactoring

public class InvoicePrinter {
    public void printInvoice(Order order) {
        printHeader(order);
        printItems(order);
        printFooter(order);
    }

    private void printHeader(Order order) {
        System.out.println("Invoice for Order #" + order.getId());
    }

    private void printItems(Order order) {
        for (Item item : order.getItems()) {
            double price = item.getPrice() * item.getQuantity();
            System.out.println(item.getName() + ": " + price);
        }
    }

    private void printFooter(Order order) {
        System.out.println("Total: " + order.getTotal());
    }
}

Benefits

Extract Class: Improving Cohesion

When a class has multiple responsibilities or grows too large, it violates the Single Responsibility Principle. In such cases, you can use the Extract Class technique to move related behavior and data into a new class.

Before Refactoring

public class Person {
    private String name;
    private String email;
    private String street;
    private String city;
    private String zip;

    public String getFullAddress() {
        return street + ", " + city + " " + zip;
    }

    // Getters and setters...
}

Here, the Person class is managing both personal identity and address information.

After Refactoring

public class Address {
    private String street;
    private String city;
    private String zip;

    public String getFullAddress() {
        return street + ", " + city + " " + zip;
    }

    // Getters and setters...
}

public class Person {
    private String name;
    private String email;
    private Address address;

    public String getAddressDetails() {
        return address.getFullAddress();
    }

    // Getters and setters...
}

Benefits

Extract Interface: Enhancing Flexibility and Decoupling

Interfaces allow code to depend on abstractions rather than concrete implementations. The Extract Interface technique involves identifying methods that define a contract for interaction and isolating them into a separate interface.

Before Refactoring

public class FileLogger {
    public void log(String message) {
        System.out.println("LOG: " + message);
    }
}

Suppose you want to allow different logging mechanisms without changing the client code.

After Refactoring

public interface Logger {
    void log(String message);
}

public class FileLogger implements Logger {
    public void log(String message) {
        System.out.println("LOG: " + message);
    }
}

public class DatabaseLogger implements Logger {
    public void log(String message) {
        // Simulated DB log
        System.out.println("DB LOG: " + message);
    }
}

Now, client code can work with the Logger interface:

public class OrderService {
    private Logger logger;

    public OrderService(Logger logger) {
        this.logger = logger;
    }

    public void processOrder() {
        logger.log("Order processed.");
    }
}

Benefits

Best Practices and Considerations

Conclusion

Refactoring with Extract Method, Extract Class, and Extract Interface empowers developers to evolve codebases into modular, clean, and extensible designs. These techniques enhance maintainability by aligning with core object-oriented principles like separation of concerns, encapsulation, and abstraction. By mastering and routinely applying these techniques, developers can transform even the most tangled code into well-structured and robust software.

Index

15.3 Replace Conditional with Polymorphism

Replacing Conditional Logic with Polymorphism

In object-oriented programming, large conditional statements like if-else and switch often signal missed opportunities for design improvement. These structures tend to grow unwieldy over time, making code harder to understand, modify, and extend. One powerful refactoring technique is to replace conditional logic with polymorphism, allowing behavior to be delegated to objects rather than being controlled by conditional flow.

By leveraging polymorphism through interfaces and class hierarchies, developers can isolate behaviors into dedicated classes. This results in code that adheres more closely to the Open/Closed Principle — open for extension but closed for modification.

The Problem with Conditionals

Consider the following code that calculates the shipping cost based on shipping type:

public class ShippingCalculator {
    public double calculateShipping(String method, double weight) {
        if (method.equals("standard")) {
            return weight * 1.0;
        } else if (method.equals("express")) {
            return weight * 1.5 + 10;
        } else if (method.equals("overnight")) {
            return weight * 2.0 + 25;
        } else {
            throw new IllegalArgumentException("Unknown shipping method: " + method);
        }
    }
}

This design is functional but rigid. Adding new shipping types requires modifying the calculateShipping method, which violates the Open/Closed Principle. Furthermore, it's harder to test and maintain each logic path individually.

Step-by-Step Refactoring to Polymorphism

Step 1: Define an Interface

public interface ShippingStrategy {
    double calculate(double weight);
}

This interface defines a common contract that all shipping strategies will implement.

Step 2: Create Concrete Strategy Classes

public class StandardShipping implements ShippingStrategy {
    public double calculate(double weight) {
        return weight * 1.0;
    }
}

public class ExpressShipping implements ShippingStrategy {
    public double calculate(double weight) {
        return weight * 1.5 + 10;
    }
}

public class OvernightShipping implements ShippingStrategy {
    public double calculate(double weight) {
        return weight * 2.0 + 25;
    }
}

Each class encapsulates its behavior, removing the need for conditionals.

Step 3: Update the Client Code

public class ShippingCalculator {
    private ShippingStrategy strategy;

    public ShippingCalculator(ShippingStrategy strategy) {
        this.strategy = strategy;
    }

    public double calculate(double weight) {
        return strategy.calculate(weight);
    }
}

Usage:

ShippingStrategy strategy = new ExpressShipping();
ShippingCalculator calculator = new ShippingCalculator(strategy);
double cost = calculator.calculate(5.0);

This approach makes it trivial to add new strategies by creating a new class that implements ShippingStrategy.

Benefits of This Refactoring

Improved Maintainability

Each class has a single responsibility. If shipping logic changes, it’s isolated to a specific class without affecting others.

Open for Extension

Adding new shipping types doesn’t require altering existing logic—just a new class.

Enhanced Testability

Each strategy can be tested independently of the others, improving coverage and reducing side effects.

Cleaner Code

Replacing nested or sprawling conditionals with dedicated classes results in clearer, self-documenting logic.

When to Apply This Pattern

This refactoring is particularly effective when:

It may be unnecessary for very simple conditionals or when behavior doesn't vary much.

Real-World Examples

Conclusion

Replacing conditionals with polymorphism is a fundamental object-oriented design technique. It helps eliminate rigid control flow and replaces it with a flexible, extensible architecture built on interfaces and inheritance. The result is more modular, testable, and scalable code that aligns with core design principles such as Single Responsibility and Open/Closed. As systems grow, this approach pays dividends in reduced complexity and easier evolution of software behavior.

Index

15.4 Applying Patterns Through Refactoring

Applying Patterns Through Refactoring

Refactoring is more than cleaning up code—it’s a path to discovering better structure. As systems grow in complexity, refactoring becomes an essential tool to evolve procedural or tightly coupled code into well-structured, maintainable designs. One powerful outcome of thoughtful refactoring is the emergence—or intentional introduction—of object-oriented design patterns.

Design patterns are proven solutions to recurring design problems. However, code often begins without patterns, especially in early prototypes or legacy systems. Through refactoring, we can reshape existing code to align with patterns such as Strategy, Observer, Command, or Decorator—enhancing clarity, testability, and extensibility.

Recognizing Opportunities for Patterns

When working with legacy code, you may encounter symptoms like long conditional chains, repeated logic, or tightly coupled classes. These are clues that a design pattern might provide a cleaner solution.

Example: Refactoring to the Strategy Pattern

Consider a legacy tax calculator:

public class TaxCalculator {
    public double calculate(String region, double amount) {
        if (region.equals("US")) {
            return amount * 0.07;
        } else if (region.equals("EU")) {
            return amount * 0.2;
        } else if (region.equals("IN")) {
            return amount * 0.18;
        }
        return 0;
    }
}

This procedural design is difficult to extend. To support more regions or changing rules, we would need to continually modify this method.

Refactored Using the Strategy Pattern:

public interface TaxStrategy {
    double calculate(double amount);
}

public class USTax implements TaxStrategy {
    public double calculate(double amount) {
        return amount * 0.07;
    }
}

public class EUTax implements TaxStrategy {
    public double calculate(double amount) {
        return amount * 0.2;
    }
}

public class IndiaTax implements TaxStrategy {
    public double calculate(double amount) {
        return amount * 0.18;
    }
}

public class TaxCalculator {
    private TaxStrategy strategy;

    public TaxCalculator(TaxStrategy strategy) {
        this.strategy = strategy;
    }

    public double calculate(double amount) {
        return strategy.calculate(amount);
    }
}

Now, new tax regions can be added without changing existing logic, following the Open/Closed Principle. The code is cleaner, more modular, and easier to test.

Observer Pattern: Making Code Reactive

Another frequent use case is decoupling event producers and consumers, common in UI, game engines, or monitoring systems. A legacy implementation may have direct method calls like:

public class Button {
    public void click() {
        System.out.println("Button clicked");
        new Logger().log("Clicked");
        new Analytics().track("Click");
    }
}

This tightly couples the Button to multiple classes, making it rigid.

Refactored Using the Observer Pattern:

public interface ClickListener {
    void onClick();
}

public class Button {
    private List<ClickListener> listeners = new ArrayList<>();

    public void addListener(ClickListener listener) {
        listeners.add(listener);
    }

    public void click() {
        for (ClickListener listener : listeners) {
            listener.onClick();
        }
    }
}

public class Logger implements ClickListener {
    public void onClick() {
        System.out.println("Logged click");
    }
}

public class Analytics implements ClickListener {
    public void onClick() {
        System.out.println("Tracked click");
    }
}

Listeners can now be registered or removed dynamically, and the button has no knowledge of who listens. This improves decoupling and allows reusability of both the button and its observers.

Benefits of Pattern-Driven Refactoring

When to Apply Patterns vs. Simpler Refactoring

Refactoring to design patterns is not always the first step. In early stages, simpler transformations like extracting methods or reducing duplication might be more appropriate. Use pattern-driven refactoring when:

Avoid forcing patterns into code where simplicity suffices. The goal is to improve structure, not complicate it with unnecessary abstractions.

Conclusion

Refactoring toward design patterns is a practical and powerful technique for evolving code. Rather than imposing patterns from the outset, let them emerge through iterative improvements. When used wisely, patterns transform rigid, duplicated logic into elegant, extensible architecture. Through small, well-directed refactorings, we enable our software to grow gracefully and sustainably.

Index