La diferencia entre un código complejo y uno simple no radica en la cantidad de líneas, sino en la claridad con la que expresa su intención. En equipos que trabajan con Java, reducir la complejidad accidental acelera el desarrollo y evita defectos reincidentes.
Este capítulo presenta ejemplos concretos de refactorización. Cada caso muestra primero una versión innecesariamente complicada y luego una alternativa simple y legible, siguiendo las ideas del principio KISS y apoyándose en conceptos de responsabilidad única.
El código complejo dificulta la incorporación de cambios porque obliga a comprender múltiples decisiones a la vez. Además, es más costoso de probar, alienta decisiones inconsistentes entre personas desarrolladoras y suele esconder errores sutíles.
Un síntoma habitual de complejidad es la mezcla de dominios: lógica de negocio, validaciones, acceso a datos y formato de salida en un mismo método. Identificar estos acoplamientos nos ayuda a planear una refactorización progresiva.
La versión compleja mezcla lógica de negocio con detalles temporales, utiliza demasiadas estructuras anidadas y se apoya en condicionales difíciles de seguir.
// Versión compleja: mezcla múltiples responsabilidades
class OrderProcessor {
private final Database database;
private final Clock clock;
OrderProcessor(Database database, Clock clock) {
this.database = database;
this.clock = clock;
}
Map<String, Object> process(Map<String, Object> payload) {
Map<String, Object> result = new HashMap<>();
Optional<Order> order = database.findOrder((String) payload.get("id"));
if (order.isPresent()) {
Order existing = order.get();
if (existing.isExpired(clock.instant())) {
database.updateStatus(existing.id(), "CANCELLED");
result.put("status", "CANCELLED");
result.put("notified", sendEmail(existing.customerEmail(), "cancelled"));
result.put("audit", writeAudit(existing));
} else {
if (payload.containsKey("priority") && (boolean) payload.get("priority")) {
database.updatePriority(existing.id(), true);
}
if (payload.containsKey("discount")) {
applyDiscount(existing, (BigDecimal) payload.get("discount"));
}
database.updateStatus(existing.id(), "READY");
result.put("status", "READY");
result.put("notified", sendEmail(existing.customerEmail(), "ready"));
}
} else {
Order created = database.create(payload);
result.put("status", "CREATED");
result.put("notified", sendEmail((String) payload.get("email"), "created"));
}
result.put("timestamp", clock.instant());
return result;
}
private boolean sendEmail(String to, String type) {
// implementación omitida
return true;
}
private String writeAudit(Order order) {
// implementación omitida
return "ok";
}
private void applyDiscount(Order order, BigDecimal discount) {
// implementación omitida
}
}
La refactorización separa responsabilidades: un servicio que decide el nuevo estado, otro que gestiona las notificaciones y un objeto de resultado explícito. El flujo principal se vuelve lineal y fácil de probar.
// Versión simple: responsabilidades claras y flujo legible
final class OrderProcessor {
private final OrderRepository repository;
private final OrderStateDecider stateDecider;
private final OrderNotifier notifier;
private final Clock clock;
OrderProcessor(OrderRepository repository,
OrderStateDecider stateDecider,
OrderNotifier notifier,
Clock clock) {
this.repository = repository;
this.stateDecider = stateDecider;
this.notifier = notifier;
this.clock = clock;
}
OrderProcessingResult process(OrderCommand command) {
Order order = repository.findOrCreate(command);
OrderStateChange change = stateDecider.decide(order, command, clock.instant());
repository.persist(change);
notifier.notify(order.customerEmail(), change.status());
return OrderProcessingResult.from(change, clock.instant());
}
}
Con el diseño simplificado, cada colaboración tiene una responsabilidad concreta. Extender la lógica implica trabajar sobre clases específicas, sin tocar todo el flujo.
El siguiente código complejo produce reportes combinando múltiples pasos en un mismo método. Cualquier ajuste exige modificar condicionales y detalles de formato simultáneamente.
// Versión compleja: múltiples formateos y cálculos mezclados
class ReportService {
private final List<Map<String, Object>> data;
ReportService(List<Map<String, Object>> data) {
this.data = data;
}
String generate() {
StringBuilder builder = new StringBuilder();
double total = 0D;
builder.append("ID;CLIENT;TOTAL;CATEGORY\n");
for (Map<String, Object> row : data) {
double subtotal = (double) row.getOrDefault("subtotal", 0D);
double taxes = subtotal * 0.21;
double shipping = (double) row.getOrDefault("shipping", 0D);
double grand = subtotal + taxes + shipping;
total += grand;
builder.append(row.get("id")).append(';')
.append(row.get("client")).append(';')
.append(grand).append(';')
.append(classify(grand)).append('\n');
}
builder.append("TOTAL;;").append(total);
return builder.toString();
}
private String classify(double amount) {
if (amount > 5000D) {
return "PREMIUM";
}
if (amount > 1000D) {
return "STANDARD";
}
return "BASIC";
}
}
La versión simple divide el problema en tres pasos: cálculos, clasificación y formato. Cada etapa reporta su intención con nombres claros.
// Versión simple: etapas explícitas y reutilizables
final class ReportGenerator {
private final TotalsCalculator totalsCalculator;
private final CategoryClassifier classifier;
private final ReportFormatter formatter;
ReportGenerator(TotalsCalculator totalsCalculator,
CategoryClassifier classifier,
ReportFormatter formatter) {
this.totalsCalculator = totalsCalculator;
this.classifier = classifier;
this.formatter = formatter;
}
String generate(List<OrderSummary> summaries) {
List<ReportRow> rows = summaries.stream()
.map(totalsCalculator::enrich)
.map(row -> row.withCategory(classifier.classify(row.total())))
.toList();
BigDecimal total = rows.stream()
.map(ReportRow::total)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return formatter.format(rows, total);
}
}
Separar los pasos permite reutilizar la lógica de cálculo y clasificación en otros contextos. El formateo queda encapsulado, lo que facilita cambiar a otro formato (por ejemplo, JSON) sin alterar la lógica.
Refactorizar de lo complejo a lo simple no es un evento único, sino un hábito. Al sostener este ejercicio, reducimos la deuda técnica y mantenemos la capacidad de respuesta del equipo.