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.