Skip to main content

Chapter 2: Code Refactoring Exercises

Objective

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​

TechniqueExplanation
Guard clausesreturn statements at the start of the method eliminate invalid cases early
Extract methodcalculateTotal() is extracted into its own method
Stream APIReplaces the for loop with a functional pipeline
"ACTIVE".equals()Reverses the call to avoid a NullPointerException
Interview phrase

"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​

TechniqueExplanation
Strategy PatternEach payment type is encapsulated in its own class/lambda
Open/Closed PrincipleAdding a new type = adding 1 line in the Map
LambdasFor simple cases, lambdas are used instead of classes
RegistryThe Map serves as a registry to find the correct calculator
Interview phrase

"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​

TechniqueExplanation
Single ResponsibilityEach class has one reason to change
BreakdownValidator, Repository, EmailService, Service (orchestrator)
TestabilityEach component can be tested independently
MaintainabilityChanging email logic doesn't affect persistence
Interview phrase

"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);
}
Interview phrase

"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
Interview phrase

"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​

PrincipleExerciseTechnique
S — Single ResponsibilityEx. 3 (God Class)Break down into specialized classes
O — Open/ClosedEx. 2 (Strategy)Add without modifying
L — Liskov SubstitutionEx. 5 (Composition)Composition over inheritance
I — Interface SegregationEx. 2 (FeeCalculator)Small, focused interfaces
D — Dependency InversionEx. 3 (UserService)Depend on abstractions, not implementations

⬅️ Chapter 1: JPA & Database | Chapter 3: Algorithms →