Pular para o conteúdo principal

Capítulo 2: Exercícios de Refatoração de Código

Objetivo

Estes exercícios simulam situações reais de entrevista onde você recebe um código ruim e precisa refatorá-lo. Cada exercício inclui o código problemático, a solução e uma explicação clara do que foi melhorado.


Exercício 1 — Eliminar Ifs Aninhados (Guard Clauses)​

"Este código está difícil de ler. Refatore-o."

O código ruim​

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);
}
}
}
}
}

Problema: "Pyramid of Doom" — os if aninhados tornam o código ilegível e difícil de manter.

O código refatorado​

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();
}

O que foi feito​

TécnicaExplicação
Guard clausesOs return no início do método eliminam casos inválidos cedo
Extract methodcalculateTotal() foi extraído para seu próprio método
Stream APISubstitui o loop for por um pipeline funcional
"ACTIVE".equals()Inverte a chamada para evitar um NullPointerException
Frase de entrevista

"Uso guard clauses para tratar casos de borda cedo e manter o caminho feliz sem indentação. Também extraio métodos auxiliares para melhorar a legibilidade e testabilidade."


Exercício 2 — Substituir If/Else pelo Strategy Pattern​

"Este código precisa suportar novos tipos de pagamento facilmente. Refatore-o."

O código ruim​

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);
}
}

Problema: Cada novo tipo de pagamento = modificar este método. Viola o princípio Open/Closed (aberto para extensão, fechado para modificação).

O código refatorado​

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 com lambdas para casos simples
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);
}

O que foi feito​

TécnicaExplicação
Strategy PatternCada tipo de pagamento é encapsulado em sua própria classe/lambda
Open/Closed PrincipleAdicionar um novo tipo = adicionar 1 linha no Map
LambdasPara casos simples, usamos lambdas ao invés de classes
RegistryO Map serve como registro para encontrar o calculador correto
Frase de entrevista

"Substituí a cadeia de if/else por um Strategy Pattern. Adicionar um novo tipo de pagamento agora é uma linha no registry, sem modificar código existente — isso é o Open/Closed Principle."


Exercício 3 — Quebrar uma God Class (SRP)​

"Esta classe faz coisas demais. Refatore-a."

O código ruim​

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");

// Criação
User user = new User(name, email);

// Persistência direta
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;
}
}

Problema: Esta classe faz 4 coisas: validação, persistência, envio de e-mail e logging. Viola o Single Responsibility Principle.

O código refatorado​

// 1. Validação
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. Persistência
@Repository
class UserRepository {
void save(User user) {
jdbcTemplate.update(
"INSERT INTO users (name, email) VALUES (?, ?)",
user.getName(), user.getEmail()
);
}
}

// 3. Notificação
@Service
class WelcomeEmailService {
void sendWelcome(User user) {
emailSender.send(user.getEmail(), "Welcome!", "Hello " + user.getName());
}
}

// 4. Orquestração (a única responsabilidade do 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;
}
}

O que foi feito​

TécnicaExplicação
Single ResponsibilityCada classe tem uma única razão para mudar
QuebraValidator, Repository, EmailService, Service (orquestrador)
TestabilidadeCada componente pode ser testado independentemente
ManutenibilidadeAlterar a lógica de e-mail não afeta a persistência
Frase de entrevista

"Apliquei o Single Responsibility Principle. O UserService agora apenas orquestra — validação, persistência e notificação estão cada uma em sua própria classe. Isso torna cada parte testável e de fácil manutenção."


Exercício 4 — Substituir Constantes Mágicas​

"Este código usa números mágicos em todo lugar. Refatore-o."

O código ruim​

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;
}
}

O código refatorado​

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);
}
}

// Uso
public double calculateShipping(double weight, ShippingZone zone) {
return zone.getRate(weight);
}
Frase de entrevista

"Substituí números mágicos por enums e tabelas de consulta. O código agora é autoexplicativo e adicionar uma nova zona é só adicionar um valor no enum."


Exercício 5 — Substituir Herança por Composição​

"Esta hierarquia de classes é rígida. Refatore-a."

O código ruim​

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"); }
}
// Problema: um pato voa E nada. Herança múltipla é impossível em Java.

O código refatorado (Composição)​

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);
}
}

// Um pato pode voar E nadar
Animal duck = new Animal("Duck", new FlyBehavior(), new SwimBehavior());
duck.performAll(); // Flying + Swimming
Frase de entrevista

"Prefiro composição à herança. Em vez de uma hierarquia rígida de classes, componho comportamentos. Isso segue o princípio 'favor composition over inheritance' do Gang of Four."


Resumo dos princípios SOLID aplicados​

PrincípioExercícioTécnica
S — Single ResponsibilityEx. 3 (God Class)Quebra em classes especializadas
O — Open/ClosedEx. 2 (Strategy)Adicionar sem modificar
L — Liskov SubstitutionEx. 5 (Composição)Composição em vez de herança
I — Interface SegregationEx. 2 (FeeCalculator)Interfaces pequenas e focadas
D — Dependency InversionEx. 3 (UserService)Depender de abstrações, não de implementações

⬅️ Capítulo 1: JPA & Banco de Dados | Capítulo 3: Algoritmos →