Capítulo 2: Exercícios de Refatoração de Código
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écnica | Explicação |
|---|---|
| Guard clauses | Os return no início do método eliminam casos inválidos cedo |
| Extract method | calculateTotal() foi extraído para seu próprio método |
| Stream API | Substitui o loop for por um pipeline funcional |
| "ACTIVE".equals() | Inverte a chamada para evitar um NullPointerException |
"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écnica | Explicação |
|---|---|
| Strategy Pattern | Cada tipo de pagamento é encapsulado em sua própria classe/lambda |
| Open/Closed Principle | Adicionar um novo tipo = adicionar 1 linha no Map |
| Lambdas | Para casos simples, usamos lambdas ao invés de classes |
| Registry | O Map serve como registro para encontrar o calculador correto |
"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écnica | Explicação |
|---|---|
| Single Responsibility | Cada classe tem uma única razão para mudar |
| Quebra | Validator, Repository, EmailService, Service (orquestrador) |
| Testabilidade | Cada componente pode ser testado independentemente |
| Manutenibilidade | Alterar a lógica de e-mail não afeta a persistência |
"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);
}
"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
"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ípio | Exercício | Técnica |
|---|---|---|
| S — Single Responsibility | Ex. 3 (God Class) | Quebra em classes especializadas |
| O — Open/Closed | Ex. 2 (Strategy) | Adicionar sem modificar |
| L — Liskov Substitution | Ex. 5 (Composição) | Composição em vez de herança |
| I — Interface Segregation | Ex. 2 (FeeCalculator) | Interfaces pequenas e focadas |
| D — Dependency Inversion | Ex. 3 (UserService) | Depender de abstrações, não de implementações |
⬅️ Capítulo 1: JPA & Banco de Dados | Capítulo 3: Algoritmos →