Chapter 2: Code Refactoring Exercises
These exercises simulate real interview situations where you are shown bad code and asked to refactor it. Each exercise includes the problematic code, the solution, and a clear explanation of what has been improved.
Exercise 1 — Eliminate Nested Ifs (Guard Clauses)
"This code is hard to read. Refactor it."
The bad code
public void processOrder(Order order) {
if (order != null) {
if (order.getStatus().equals("ACTIVE")) {
if (order.getItems() != null && !order.getItems().isEmpty()) {
if (order.getCustomer() != null) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
order.setTotal(total);
emailService.sendConfirmation(order.getCustomer().getEmail(), total);
}
}
}
}
}
Problem: "Pyramid of Doom" — the nested if statements make the code unreadable and hard to maintain.
The refactored code
public void processOrder(Order order) {
if (order == null) return;
if (!"ACTIVE".equals(order.getStatus())) return;
if (order.getItems() == null || order.getItems().isEmpty()) return;
if (order.getCustomer() == null) return;
double total = calculateTotal(order.getItems());
order.setTotal(total);
emailService.sendConfirmation(order.getCustomer().getEmail(), total);
}
private double calculateTotal(List<Item> items) {
return items.stream()
.mapToDouble(i -> i.getPrice() * i.getQuantity())
.sum();
}
What was done
| Technique | Explanation |
|---|---|
| Guard clauses | return statements at the start of the method eliminate invalid cases early |
| Extract method | calculateTotal() is extracted into its own method |
| Stream API | Replaces the for loop with a functional pipeline |
| "ACTIVE".equals() | Reverses the call to avoid a NullPointerException |
"I use guard clauses to handle edge cases early and keep the happy path unindented. I also extract helper methods to improve readability and testability."
Exercise 2 — Replace If/Else with the Strategy Pattern
"This code needs to easily support new payment types. Refactor it."
The bad code
public double calculateFee(String paymentType, double amount) {
if (paymentType.equals("CREDIT_CARD")) {
return amount * 0.03;
} else if (paymentType.equals("PAYPAL")) {
return amount * 0.025;
} else if (paymentType.equals("BANK_TRANSFER")) {
return amount * 0.01;
} else if (paymentType.equals("CRYPTO")) {
return amount * 0.005;
} else {
throw new IllegalArgumentException("Unknown: " + paymentType);
}
}
Problem: Each new payment type = modifying this method. Violates the Open/Closed principle (open for extension, closed for modification).
The refactored code
interface FeeCalculator {
double calculate(double amount);
}
class CreditCardFee implements FeeCalculator {
public double calculate(double amount) { return amount * 0.03; }
}
class PaypalFee implements FeeCalculator {
public double calculate(double amount) { return amount * 0.025; }
}
// Registry with lambdas for simple cases
Map<String, FeeCalculator> calculators = Map.of(
"CREDIT_CARD", new CreditCardFee(),
"PAYPAL", new PaypalFee(),
"BANK_TRANSFER", amount -> amount * 0.01,
"CRYPTO", amount -> amount * 0.005
);
public double calculateFee(String type, double amount) {
FeeCalculator calc = calculators.get(type);
if (calc == null) throw new IllegalArgumentException("Unknown: " + type);
return calc.calculate(amount);
}
What was done
| Technique | Explanation |
|---|---|
| Strategy Pattern | Each payment type is encapsulated in its own class/lambda |
| Open/Closed Principle | Adding a new type = adding 1 line in the Map |
| Lambdas | For simple cases, lambdas are used instead of classes |
| Registry | The Map serves as a registry to find the correct calculator |
"I replaced the if/else chain with a Strategy Pattern. Adding a new payment type is now a single line in the registry, without modifying existing code — that's the Open/Closed Principle."
Exercise 3 — Break Down a God Class (SRP)
"This class does too much. Refactor it."
The bad code
public class UserService {
public User createUser(String name, String email) {
// Validation
if (name == null || email == null) throw new IllegalArgumentException();
if (!email.contains("@")) throw new IllegalArgumentException("Bad email");
// Creation
User user = new User(name, email);
// Direct persistence
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
jdbcTemplate.update(sql, name, email);
// Email
String subject = "Welcome!";
String body = "Hello " + name;
emailSender.send(email, subject, body);
// Logging
log.info("User created: " + email);
return user;
}
}
Problem: This class does 4 things: validation, persistence, email sending, and logging. Violates the Single Responsibility Principle.
The refactored code
// 1. Validation
class UserValidator {
void validate(String name, String email) {
Objects.requireNonNull(name, "name required");
Objects.requireNonNull(email, "email required");
if (!email.contains("@")) throw new IllegalArgumentException("Bad email");
}
}
// 2. Persistence
@Repository
class UserRepository {
void save(User user) {
jdbcTemplate.update(
"INSERT INTO users (name, email) VALUES (?, ?)",
user.getName(), user.getEmail()
);
}
}
// 3. Notification
@Service
class WelcomeEmailService {
void sendWelcome(User user) {
emailSender.send(user.getEmail(), "Welcome!", "Hello " + user.getName());
}
}
// 4. Orchestration (the only responsibility of the Service)
@Service
public class UserService {
private final UserValidator validator;
private final UserRepository repo;
private final WelcomeEmailService welcomeEmail;
@Transactional
public User createUser(String name, String email) {
validator.validate(name, email);
User user = new User(name, email);
repo.save(user);
welcomeEmail.sendWelcome(user);
return user;
}
}
What was done
| Technique | Explanation |
|---|---|
| Single Responsibility | Each class has one reason to change |
| Breakdown | Validator, Repository, EmailService, Service (orchestrator) |
| Testability | Each component can be tested independently |
| Maintainability | Changing email logic doesn't affect persistence |
"I applied the Single Responsibility Principle. The UserService now only orchestrates — validation, persistence, and notification are each in their own class. This makes each piece independently testable and maintainable."
Exercise 4 — Replace Magic Numbers
"This code uses magic numbers everywhere. Refactor it."
The bad code
public double calculateShipping(double weight, int zone) {
if (zone == 1) {
if (weight <= 5) return 4.99;
else if (weight <= 20) return 9.99;
else return 14.99;
} else if (zone == 2) {
if (weight <= 5) return 7.99;
else if (weight <= 20) return 14.99;
else return 24.99;
} else {
return 29.99;
}
}
The refactored code
public enum ShippingZone {
LOCAL(1, Map.of(
WeightCategory.LIGHT, 4.99,
WeightCategory.MEDIUM, 9.99,
WeightCategory.HEAVY, 14.99
)),
NATIONAL(2, Map.of(
WeightCategory.LIGHT, 7.99,
WeightCategory.MEDIUM, 14.99,
WeightCategory.HEAVY, 24.99
));
private final int code;
private final Map<WeightCategory, Double> rates;
public double getRate(double weight) {
return rates.get(WeightCategory.fromWeight(weight));
}
}
enum WeightCategory {
LIGHT(0, 5), MEDIUM(5, 20), HEAVY(20, Double.MAX_VALUE);
static WeightCategory fromWeight(double weight) {
return Arrays.stream(values())
.filter(c -> weight > c.min && weight <= c.max)
.findFirst()
.orElse(HEAVY);
}
}
// Usage
public double calculateShipping(double weight, ShippingZone zone) {
return zone.getRate(weight);
}
"I replaced magic numbers with enums and lookup tables. The code is now self-documenting and adding a new zone is just adding an enum value."
Exercise 5 — Replace Inheritance with Composition
"This class hierarchy is rigid. Refactor it."
The bad code
class Animal {
void eat() { System.out.println("Eating"); }
}
class FlyingAnimal extends Animal {
void fly() { System.out.println("Flying"); }
}
class SwimmingAnimal extends Animal {
void swim() { System.out.println("Swimming"); }
}
// Problem: a duck flies AND swims. Multiple inheritance is impossible in Java.
The refactored code (Composition)
interface Behavior {
void perform();
}
class FlyBehavior implements Behavior {
public void perform() { System.out.println("Flying"); }
}
class SwimBehavior implements Behavior {
public void perform() { System.out.println("Swimming"); }
}
class Animal {
private final String name;
private final List<Behavior> behaviors;
Animal(String name, Behavior... behaviors) {
this.name = name;
this.behaviors = List.of(behaviors);
}
void performAll() {
behaviors.forEach(Behavior::perform);
}
}
// A duck can fly AND swim
Animal duck = new Animal("Duck", new FlyBehavior(), new SwimBehavior());
duck.performAll(); // Flying + Swimming
"I favor composition over inheritance. Instead of a rigid class hierarchy, I compose behaviors. This follows the 'favor composition over inheritance' principle from the Gang of Four."
Summary of Applied SOLID Principles
| Principle | Exercise | Technique |
|---|---|---|
| S — Single Responsibility | Ex. 3 (God Class) | Break down into specialized classes |
| O — Open/Closed | Ex. 2 (Strategy) | Add without modifying |
| L — Liskov Substitution | Ex. 5 (Composition) | Composition over inheritance |
| I — Interface Segregation | Ex. 2 (FeeCalculator) | Small, focused interfaces |
| D — Dependency Inversion | Ex. 3 (UserService) | Depend on abstractions, not implementations |