Introduction
Refactoring is the process of changing the internal structure of code without changing its external behavior. It is not rewriting. It is not adding features. It is a disciplined, step-by-step improvement of existing code.
The golden rule: refactor only when you have tests. Without a safety net, you are just editing and praying.
1. Extract Method
The most common and most powerful refactoring technique. When a block of code can be grouped together and named, extract it.
Before
public void printInvoice(Invoice invoice) {
System.out.println("====== INVOICE ======");
System.out.println("Customer: " + invoice.getCustomerName());
System.out.println("Date: " + invoice.getDate());
double total = 0;
for (LineItem item : invoice.getItems()) {
double lineTotal = item.getPrice() * item.getQuantity();
System.out.println(item.getName() + " x" + item.getQuantity() + " = " + lineTotal);
total += lineTotal;
}
double tax = total * 0.18;
double grandTotal = total + tax;
System.out.println("Subtotal: " + total);
System.out.println("Tax (18%): " + tax);
System.out.println("Grand Total: " + grandTotal);
}
After
public void printInvoice(Invoice invoice) {
printHeader(invoice);
double subtotal = printLineItems(invoice.getItems());
printTotals(subtotal);
}
private void printHeader(Invoice invoice) {
System.out.println("====== INVOICE ======");
System.out.println("Customer: " + invoice.getCustomerName());
System.out.println("Date: " + invoice.getDate());
}
private double printLineItems(List<LineItem> items) {
double total = 0;
for (LineItem item : items) {
double lineTotal = item.getPrice() * item.getQuantity();
System.out.println(item.getName() + " x" + item.getQuantity() + " = " + lineTotal);
total += lineTotal;
}
return total;
}
private void printTotals(double subtotal) {
double tax = subtotal * 0.18;
System.out.println("Subtotal: " + subtotal);
System.out.println("Tax (18%): " + tax);
System.out.println("Grand Total: " + (subtotal + tax));
}
The main method now reads like an outline. Each helper does exactly one thing.
2. Extract Class
When a class has too many responsibilities, split it into two or more focused classes.
Before
public class Employee {
private String name;
private String email;
private String street;
private String city;
private String zipCode;
private String country;
private double salary;
private String bankAccount;
private String taxId;
public String getFullAddress() {
return street + ", " + city + " " + zipCode + ", " + country;
}
public double calculateNetSalary() {
return salary - (salary * getTaxRate());
}
private double getTaxRate() { /* complex tax logic */ return 0.3; }
}
After
public class Employee {
private String name;
private String email;
private Address address;
private PayrollInfo payroll;
}
public class Address {
private String street;
private String city;
private String zipCode;
private String country;
public String getFullAddress() {
return street + ", " + city + " " + zipCode + ", " + country;
}
}
public class PayrollInfo {
private double salary;
private String bankAccount;
private String taxId;
public double calculateNetSalary() {
return salary - (salary * getTaxRate());
}
}
3. Replace Conditionals with Polymorphism
When you see a switch statement or if-else chain that selects behavior based on type, replace it with polymorphism.
Before
public class NotificationService {
public void send(String channel, String message, String recipient) {
if ("EMAIL".equals(channel)) {
EmailClient client = new EmailClient();
client.sendEmail(recipient, "Notification", message);
} else if ("SMS".equals(channel)) {
SmsGateway gateway = new SmsGateway();
gateway.sendSms(recipient, message);
} else if ("PUSH".equals(channel)) {
PushService push = new PushService();
push.sendPush(recipient, message);
} else if ("SLACK".equals(channel)) {
SlackApi slack = new SlackApi();
slack.postMessage(recipient, message);
}
}
}
After
public interface NotificationChannel {
void send(String recipient, String message);
}
public class EmailNotification implements NotificationChannel {
public void send(String recipient, String message) {
new EmailClient().sendEmail(recipient, "Notification", message);
}
}
public class SmsNotification implements NotificationChannel {
public void send(String recipient, String message) {
new SmsGateway().sendSms(recipient, message);
}
}
// Adding Slack or Push = new class, no existing code changes
public class NotificationService {
private final Map<String, NotificationChannel> channels;
public void send(String channelName, String message, String recipient) {
NotificationChannel channel = channels.get(channelName);
if (channel == null) throw new IllegalArgumentException("Unknown channel: " + channelName);
channel.send(recipient, message);
}
}
4. Introduce Parameter Object
When multiple parameters travel together through method signatures, group them into an object.
Before
public List<Transaction> search(
String accountId,
LocalDate startDate,
LocalDate endDate,
String transactionType,
double minAmount,
double maxAmount,
String sortBy,
String sortOrder
) { ... }
After
public record TransactionSearchCriteria(
String accountId,
LocalDate startDate,
LocalDate endDate,
String transactionType,
double minAmount,
double maxAmount,
String sortBy,
String sortOrder
) {}
public List<Transaction> search(TransactionSearchCriteria criteria) { ... }
This also makes it easy to add defaults, validation, and builder patterns.
5. Inline Method
The opposite of Extract Method. When a method body is as clear as its name, inline it.
Before
public boolean isEligibleForDiscount(Customer customer) {
return isAdult(customer);
}
private boolean isAdult(Customer customer) {
return customer.getAge() >= 18;
}
After
public boolean isEligibleForDiscount(Customer customer) {
return customer.getAge() >= 18;
}
Only inline when the extracted method adds no clarity. If the name communicates intent better than the code, keep it.
Interview Questions
-
When should you refactor? Follow the Rule of Three: the first time you do something, just do it. The second time, note the duplication. The third time, refactor.
-
What is the difference between refactoring and rewriting? Refactoring preserves behavior while improving structure. Rewriting discards the old code and starts fresh. Refactoring is incremental and safe; rewriting is risky.
-
How do you refactor legacy code without tests? Start by writing "characterization tests" — tests that document the current behavior (even if buggy). Then refactor with confidence.
Common Mistakes
- Big-bang refactoring: Trying to refactor everything at once. Always do it in small, tested steps.
- Refactoring without a clear goal: Know why you are refactoring. "Make it cleaner" is too vague. "Extract the pricing logic so it can be unit tested" is specific.
- Skipping the commit: Commit after each successful refactoring step. If something breaks, you can revert to a working state.
Summary
| Technique | When to Use |
|---|---|
| Extract Method | Long method, repeated code block |
| Extract Class | Class with too many responsibilities |
| Replace Conditionals | Type-based if-else / switch chains |
| Introduce Parameter Object | 4+ parameters traveling together |
| Inline Method | Wrapper adds no clarity |