Faster, better AI-powered code reviews. Start your free trial!  
Faster, better AI-powered code reviews.
Start your free trial!

Get high quality AI code reviews

Code Smells Refactoring in Java the Right Way

Code Smells Refactoring in Java the Right Way

Table of Contents

Writing clean, maintainable code is a hallmark of an experienced Java developer. However, even seasoned programmers can sometimes produce code that “smells” – exhibits patterns that indicate weaknesses in design and implementation. While not technically bugs, these code smells suggest the need for refactoring to improve quality and reduce maintenance costs. Mastering code smell detection and refactoring techniques is a key skill for any Java professional.

This comprehensive guide will provide Java developers with:

  • An understanding of common code smells to watch out for
  • Effective refactoring techniques to eliminate bad smells
  • Tools to automate and assist the refactoring process

By learning to identify code issues early and refactor with confidence, you can boost the health of your codebase and prevent future headaches down the road. Let’s start sniffing out smelly code!

Detecting Code Smells

The first step is training your nose to detect the subtle scents of problems in your code. Here are three of the most common code smells to watch out for:

Long Methods

Methods that grow too long become difficult to understand and modify. As a rule of thumb, methods should focus on a single coherent task and not exceed 10-15 lines. Excessively long methods likely need to be broken down.

Example

public void printOrderDetails(Order order) {

  System.out.println("OrderId: " + order.id);
  System.out.println("Order Date: " + order.orderDate);  
  // print order items
  for(OrderItem item : order.items) {
    System.out.println(item.name + " (" + item.qty + ")");
  }
  // print billing address
  System.out.println("\nBilled to:");
  System.out.println(order.billingAddress.street);
  System.out.println(order.billingAddress.city);
  System.out.println(order.billingAddress.zipCode);
  // print shipping address
  System.out.println("\nShipped to:");
  System.out.println(order.shippingAddress.street);
  System.out.println(order.shippingAddress.city);
  System.out.println(order.shippingAddress.zipCode);

  // print order total
  System.out.println("\nTotal: $" + order.total);
}

This `printOrderDetails` method has grown too long. We can refactor by extracting tasks to separate methods:

public void printOrderDetails(Order order) {
  printHeader(order);
  printOrderItems(order);
  printBillingAddress(order);
  printShippingAddress(order);
  printTotal(order);
}

private void printHeader(Order order) {
System.out.println("OrderId: " + order.id);  
System.out.println("Order Date: " + order.orderDate);
}
private void printOrderItems(Order order) {
  for(OrderItem item : order.items) {
   System.out.println(item.name + " (" + item.qty + ")");
  }
}

Each method now focuses on one task. This makes the high-level workflow easier to understand.

Large Classes

Classes that accumulate excessive responsibilities become more difficult to maintain. This smells of a failure to adhere to the single responsibility principle.

Example 

public class UserManager {

  // user fields
  private List<User> users;
  
  // authentication fields
  private final TokenGenerator tokenGenerator;
  private final TokenValidator tokenValidator;
  
  // authorization fields
  private UserRoleCalculator roleCalculator;
  private RolePermissionMapper permissions;

  // user methods
  public User createUser(String name) {...}
  public void disableUser(String id) {...}

  // auth methods  
  public String generateToken(User user) {...}
  public void validateToken(String token) throws InvalidTokenException {...}  

  // authz methods
  public boolean userHasPermission(User user, String action) {...}
}

The `UserManager` class has swollen with responsibilities related to user management, authentication, and authorization. We can refactor by extracting relevant fields and methods into separate classes:

public class UserManager {
   private UserRepository userRepository;
  public User createUser(String name) {...}
  public void disableUser(String id) {...}
}

public class AuthManager {
  private final TokenGenerator tokenGenerator;
  private final TokenValidator tokenValidator;
  public String generateToken(User user) {...}
  public void validateToken(String token) throws InvalidTokenException {...}
}

public class AuthzManager {
  private UserRoleCalculator roleCalculator;
  private RolePermissionMapper permissions;
  public boolean userHasPermission(User user, String action) {...} 
}

Breaking apart bloated classes according to concerns makes the code more modular and maintainable.

Duplicate Code 

Similar or identical code structures duplicated in multiple locations indicate improper abstraction. Such duplicate code violates the DRY principle and creates maintenance issues. 

Example

public class EmailService {
  public void sendPasswordResetEmail(User user) {
    //...
    Email email = createEmail();
    email.to = user.emailAddress;
    email.subject = "Reset your password";
    email.body = createResetPasswordEmailBody(user);

    emailSender.send(email);
  }

  public void sendNotificationEmail(User user) {
    //...
    Email email = createEmail();
    email.to = user.emailAddress;
    email.subject = "New notification!"; 
    email.body = createNotificationEmailBody(user);
       emailSender.send(email);
  } 
}

The `EmailService` contains duplicate code for creating and sending `Email` objects. We can eliminate this duplication by extracting a method:

public class EmailService {

  public void sendPasswordResetEmail(User user) {
    //...
    sendEmail(user, "Reset your password", createResetPasswordEmailBody(user));
  }

  public void sendNotificationEmail(User user) {
    //... 
    sendEmail(user, "New notification!", createNotificationEmailBody(user));
  }

  private void sendEmail(User user, String subject, String body) {
    Email email = createEmail();
    email.to = user.emailAddress;
    email.subject = subject;
    email.body = body;
    emailSender.send(email);
  }
}

Now the shared email sending logic resides in one place.

Refactoring Techniques

Once code smells have been identified, we can eliminate them through refactoring. Here are some useful techniques for restructuring code:

Extract Method

Long methods can be broken down by extracting parts of their body into separate methods, each handling a single task.

Example

public void printReport(Report report) {
  // print header
  System.out.println("Report: " + report.getName());
  System.out.println("Date: " + formattedDate(report.getGeneratedOn()));

  // print body
  for (Section section : report.getSections()) {
    System.out.println(section.getHeading());
    for (Row row : section.getRows()) {
      System.out.println("  " + row.getText());
    }
  }

  // print footer  
  System.out.println("End of report");
}

We can extract the header, body, and footer printing:

public void printReport(Report report) {
  printReportHeader(report);
  
  printReportBody(report);

  printReportFooter();
}

private void printReportHeader(Report report) {
  System.out.println("Report: " + report.getName());
  System.out.println("Date: " + formattedDate(report.getGeneratedOn()));  
}

private void printReportBody(Report report) {
  for (Section section : report.getSections()) {
    System.out.println(section.getHeading());
    for (Row row : section.getRows()) {
      System.out.println("  " + row.getText()); 
    }
  }
}
  
private void printReportFooter() {
  System.out.println("End of report");
}

Each method now focuses on one printing task. The high-level logic is easier to grasp.

Move Method

Methods that access the data or functionality of another class more than their own class may need to be moved. This avoids inappropriate intimacy between classes.

Example

public class OrderService {

  private OrderRepository orderRepository;

  public int calculateLateOrders() {
    List<Order> orders = orderRepository.getAllOrders();  
    int lateOrders = 0;
	
    for (Order order : orders) {
      if (order.getDeliveryDate().before(new Date())) {
        lateOrders++;  
      }
    }
    
    return lateOrders;
  }

The `OrderService` is dealing with order delivery dates, which is order domain logic. We can move this method to the `Order` class:

public class OrderService {

  private OrderRepository orderRepository;

}

public class Order {

  public Date deliveryDate;

  public boolean isLate() {
    return deliveryDate.before(new Date());
  }

}

Now the order lateness check is properly encapsulated in the `Order` class.

Extract Class

A class with multiple responsibilities can be refactored by extracting subsets of related fields and methods into new classes.

Example

public class User {

  private String email;
  
  private String firstName;
  private String lastName;

  private String address;
  private String city;
  private String state;  
  private int zipCode;

  private String bio;

  // address methods
  public String getFullAddress() {...} 
  
  // profile methods
  public String getProfile() {...}
}

The `User` class contains address and profile related responsibilities. We can extract these into separate classes:

public class User {
  private String email;
  
  private String firstName;
  private String lastName;  

  private UserProfile userProfile;
  private UserAddress userAddress;
}

public class UserProfile {
  private String bio;  

  public String getProfile() {...}
}

public class UserAddress {
  private String address;
  private String city;
  private String state;
  private int zipCode;

  public String getFullAddress() {...}
}

This separates unrelated concerns and yields more targeted, modular classes.

Refactoring Tools

Refactoring code manually can be tedious and error-prone. Thankfully, there are tools available to automate and assist the process:

IDE Features

Modern Java IDEs like IntelliJ and Eclipse contain built-in refactoring features. For example, they can automatically extract methods or rename identifiers based on their usages across code.

External Tools 

Standalone tools like SonarQube and Checkstyle provide code analysis to detect quality and style issues. They identify problem spots in need of refactoring.

Incremental Refactoring

Try to refactor code incrementally as you add new features. This prevents accumulation of technical debt over time. Refactoring little and often is generally more manageable than fixing a huge mess later.

Make use of these tools to amplify the power of refactoring and speed up the process. But balance them with manual techniques – understanding how your IDE refactors under the hood will make you a better developer.

The Benefits of Refactoring

Refactoring produces code that is:

  • Readable- Easy to understand with clear intent
  • Maintainable – Simple to modify and extend
  • Reliable – Less likely to contain bugs
  • Efficient – No duplication or unnecessary complexity

This pays huge dividends down the road. Refactoring is a key technique for keeping code clean and flexible as a project evolves.

Conclusion

Code smells indicate areas of code that need refactoring love. By learning smell patterns and refactoring techniques, Java developers can eliminate issues early before they balloon into major problems.

Anand Das

Anand Das

Anand is Co-founder and CTO of Bito. He leads technical strategy and engineering, and is our biggest user! Formerly, Anand was CTO of Eyeota, a data company acquired by Dun & Bradstreet. He is co-founder of PubMatic, where he led the building of an ad exchange system that handles over 1 Trillion bids per day.

Amar Goel

Amar Goel

Amar is the Co-founder and CEO of Bito. With a background in software engineering and economics, Amar is a serial entrepreneur and has founded multiple companies including the publicly traded PubMatic and Komli Media.

Written by developers for developers

This article was handcrafted with by the Bito team.

Latest posts

Bito’s AI Code Review Agent now available in VS Code and JetBrains extensions

PEER REVIEW: A New Video Podcast by Engineers, for Engineers

How Can AI Handle My Large Codebase?

Elevate Code Quality with AI: Write Clean, Maintainable Code

Identifying and Fixing Scalability Issues in Pull Requests

Top posts

Bito’s AI Code Review Agent now available in VS Code and JetBrains extensions

PEER REVIEW: A New Video Podcast by Engineers, for Engineers

How Can AI Handle My Large Codebase?

Elevate Code Quality with AI: Write Clean, Maintainable Code

Identifying and Fixing Scalability Issues in Pull Requests

From the blog

The latest industry news, interviews, technologies, and resources.

Get Bito for IDE of your choice