Capítulo 2: Ejercicios de Refactorización de Código
Estos ejercicios simulan situaciones reales de entrevista donde se le muestra código deficiente y se le pide refactorizarlo. Cada ejercicio incluye el código problemático, la solución y una explicación clara de lo que se ha mejorado.
Ejercicio 1 — Eliminar Ifs anidados (Guard Clauses)
"Este código es difícil de leer. Refactórelo."
El código deficiente
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" — los if anidados hacen que el código sea ilegible y difícil de mantener.
El código refactorizado
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();
}
Qué se hizo
| Técnica | Explicación |
|---|---|
| Guard clauses | Los return al inicio del método eliminan los casos inválidos temprano |
| Extract method | calculateTotal() se extrae en su propio método |
| Stream API | Reemplaza el bucle for por un pipeline funcional |
| "ACTIVE".equals() | Invierte la llamada para evitar un NullPointerException |
"Uso guard clauses para manejar casos límite temprano y mantener el camino feliz sin sangría. También extraigo métodos auxiliares para mejorar la legibilidad y la testabilidad."
Ejercicio 2 — Reemplazar If/Else por el Strategy Pattern
"Este código debe soportar nuevos tipos de pago fácilmente. Refactórelo."
El código deficiente
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 nuevo tipo de pago = modificar este método. Viola el principio Open/Closed (abierto a extensión, cerrado a modificación).
El código refactorizado
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 con lambdas para los 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);
}
Qué se hizo
| Técnica | Explicación |
|---|---|
| Strategy Pattern | Cada tipo de pago está encapsulado en su propia clase/lambda |
| Open/Closed Principle | Agregar un nuevo tipo = agregar 1 línea en el Map |
| Lambdas | Para los casos simples, se usan lambdas en vez de clases |
| Registry | El Map sirve como registro para encontrar el calculador correcto |
"Reemplacé la cadena if/else por un Strategy Pattern. Agregar un nuevo tipo de pago ahora es una sola línea en el registro, sin modificar el código existente — eso es el Open/Closed Principle."
Ejercicio 3 — Dividir una God Class (SRP)
"Esta clase hace demasiadas cosas. Refactórela."
El código deficiente
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");
// Création
User user = new User(name, email);
// Persistance directe
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 clase hace 4 cosas: validación, persistencia, envío de email y logging. Viola el Single Responsibility Principle.
El código refactorizado
// 1. Validación
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. Persistencia
@Repository
class UserRepository {
void save(User user) {
jdbcTemplate.update(
"INSERT INTO users (name, email) VALUES (?, ?)",
user.getName(), user.getEmail()
);
}
}
// 3. Notificación
@Service
class WelcomeEmailService {
void sendWelcome(User user) {
emailSender.send(user.getEmail(), "Welcome!", "Hello " + user.getName());
}
}
// 4. Orquestación (la única responsabilidad del 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;
}
}
Qué se hizo
| Técnica | Explicación |
|---|---|
| Single Responsibility | Cada clase tiene una sola razón para cambiar |
| División | Validator, Repository, EmailService, Service (orquestador) |
| Testabilidad | Cada componente se prueba de forma independiente |
| Mantenibilidad | Cambiar la lógica de email no afecta la persistencia |
"Apliqué el Single Responsibility Principle. El UserService ahora solo orquesta — validación, persistencia y notificación están cada una en su propia clase. Esto hace que cada parte sea independiente para probar y mantener."
Ejercicio 4 — Reemplazar Constantes Mágicas
"Este código utiliza números mágicos por todas partes. Refactórelo."
El código deficiente
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;
}
}
El código refactorizado
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);
}
"Reemplacé los números mágicos por enums y tablas de consulta. El código ahora se documenta solo y agregar una nueva zona es solo agregar un valor enum."
Ejercicio 5 — Reemplazar herencia por composición
"Esta jerarquía de clases es rígida. Refactórela."
El código deficiente
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: un pato vuela Y nada. Herencia múltiple imposible en Java.
El código refactorizado (Composición)
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);
}
}
// Un pato puede volar Y nadar
Animal duck = new Animal("Duck", new FlyBehavior(), new SwimBehavior());
duck.performAll(); // Flying + Swimming
"Prefiero la composición sobre la herencia. En lugar de una jerarquía de clases rígida, compongo comportamientos. Esto sigue el principio 'favor composition over inheritance' del Gang of Four."
Resumen de los principios SOLID aplicados
| Principio | Ejercicio | Técnica |
|---|---|---|
| S — Single Responsibility | Ej. 3 (God Class) | Dividir en clases especializadas |
| O — Open/Closed | Ej. 2 (Strategy) | Agregar sin modificar |
| L — Liskov Substitution | Ej. 5 (Composición) | Composición en lugar de herencia |
| I — Interface Segregation | Ej. 2 (FeeCalculator) | Interfaces pequeñas y enfocadas |
| D — Dependency Inversion | Ej. 3 (UserService) | Depender de abstracciones, no de implementaciones |