W3docs

Principios de Código Limpio en Java

Principios de código limpio en Java: métodos pequeños, nombres significativos, responsabilidad única y mutación mínima.

El código limpio es aquel que la siguiente persona — a menudo tú mismo, seis meses después — puede leer, confiar y modificar sin temor. El compilador acepta casi cualquier cosa; el código limpio está orientado a la otra audiencia: los humanos que lo mantienen. Ninguno de los principios a continuación es un invento exclusivo de Java, pero Java te ofrece herramientas concretas — métodos pequeños, campos final, records, excepciones, tipos con significado — para aplicarlos. Este capítulo recorre los que ofrecen beneficios cada día.

Nombres que revelan la intención

Un buen nombre responde al por qué existe el valor, no solo al tipo que es. Si un nombre necesita un comentario para explicarlo, el nombre es incorrecto. Evita las letras sueltas (salvo en bucles cortos), evita las abreviaturas que solo tú entiendes y prefiere un nombre descriptivo largo sobre uno corto y críptico — tu IDE lo autocompleta de todas formas.

// Unclear: what is d? what unit? what is the magic 86400000?
int d = (t2 - t1) / 86400000;

// Clear: the names and a constant carry the meaning
long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
long elapsedDays = (endMillis - startMillis) / MILLIS_PER_DAY;

Los booleanos se leen mejor como preguntas o estados: isActive, hasNext, shouldRetry. Los métodos que hacen algo reciben nombres de verbo (calculateTotal); los métodos que devuelven algo reciben nombres de sustantivo o getter (total, getTotal). Para las convenciones del lenguaje detrás de estas decisiones, consulta convenciones de nomenclatura en Java y mejores prácticas de nomenclatura en Java.

Métodos pequeños con una única responsabilidad

Un método debe hacer una sola cosa a un único nivel de abstracción. Cuando te encuentres añadiendo un comentario como // now validate the input, ese bloque normalmente querrá convertirse en su propio método con un nombre claro. Los métodos cortos son más fáciles de nombrar, probar y reutilizar — y el punto de llamada se lee como una oración.

// Before: one method juggling validation, calculation, and formatting
String receipt(Order order) {
  if (order == null || order.items().isEmpty())
    throw new IllegalArgumentException("empty order");
  int total = 0;
  for (var i : order.items()) total += i.price() * i.qty();
  return "Total: $" + total / 100 + "." + (total % 100);
}

// After: each step is a named method; receipt() now reads top-down
String receipt(Order order) {
  requireNonEmpty(order);
  int total = totalCents(order);
  return formatCents(total);
}

Las cláusulas de guarda mantienen los métodos planos. Maneja los casos extremos y retorna anticipadamente en lugar de envolver el flujo principal en bloques if cada vez más anidados.

Preferir la inmutabilidad y la mutación mínima

El estado mutable compartido es la raíz de la mayoría de los errores de concurrencia y de mucha confusión en general. Usa final por defecto en campos y variables locales; relaja hacia lo mutable solo cuando tengas una razón. Los records de Java convierten los objetos de valor inmutables en una sola línea, generando un constructor canónico, equals, hashCode y toString.

// An immutable value object with validation in the compact constructor
record Money(long cents, String currency) {
  Money {
    if (cents < 0) throw new IllegalArgumentException("cents must be >= 0");
  }
  Money plus(Money other) { return new Money(cents + other.cents, currency); }
}

Observa que plus devuelve un nuevo Money en lugar de mutar this. Los objetos inmutables son seguros para compartir entre hilos, seguros para usar como claves de mapa e imposibles de corromper después de la construcción. Para profundizar, consulta Java records, clases inmutables y mejores prácticas de inmutabilidad; la palabra clave final cubre cómo bloquear campos y variables.

Fallar rápido y usar excepciones, no códigos de error

Valida los argumentos en el límite y lanza la excepción de inmediato cuando algo está mal, para que el fallo aparezca cerca de su causa en lugar de tres capas más abajo como un confuso NullPointerException. Usa excepciones para señalar errores; no devuelvas null ni valores centinela mágicos que cada llamador deba recordar comprobar.

PatrónEvitarPreferir
Valor ausentereturn null;Optional<T> o lanzar excepción
Argumento incorrectoreturn -1;throw new IllegalArgumentException(...)
Estado imposiblevalor predeterminado silenciosothrow new IllegalStateException(...)
Limpieza de recursosfinally manualtry-with-resources
static User findUser(String id) {
  Objects.requireNonNull(id, "id must not be null");   // fail fast
  return repository.lookup(id)
      .orElseThrow(() -> new NoSuchElementException("no user: " + id));
}

Objects.requireNonNull convierte un NPE vago aguas abajo en un mensaje preciso en el punto de entrada. Para modelar "puede estar ausente" sin null, lee Java Optional; para diseñar tipos de error, consulta mejores prácticas con excepciones y excepciones personalizadas.

DRY, pero sin abstraer en exceso

DRY (Don't Repeat Yourself) significa que un único fragmento de conocimiento vive en un solo lugar. Cuando la misma constante o cálculo aparece dos veces, extráelo. Pero resiste el error opuesto: dos fragmentos que hoy simplemente parecen iguales pueden divergir mañana. El código duplicado es más barato de corregir que la abstracción incorrecta. Extrae cuando el significado es compartido, no solo la sintaxis.

// Knowledge duplicated: the threshold lives in two places
if (order.total() >= 5000) freeShip = true;     // here
if (cart.total() >= 5000) showBadge = true;     // and here

// One source of truth
static final int FREE_SHIPPING_THRESHOLD_CENTS = 5000;
boolean qualifiesForFreeShipping(int totalCents) {
  return totalCents >= FREE_SHIPPING_THRESHOLD_CENTS;
}

Un ejemplo completo: un carrito de compras limpio

Este programa reúne todos los principios en un único archivo pequeño y ejecutable: un record LineItem inmutable que se valida a sí mismo, métodos con una sola responsabilidad y nombres que revelan la intención, constantes con nombre en lugar de números mágicos, una cláusula de guarda e igualdad basada en valor. No se necesitan comentarios para explicar qué hace — los nombres se encargan de eso.

java— editable, runs on the server

Lo que se obtiene de la ejecución:

  • La salida se lee exactamente como el dominio que modela — 2 x Notebook @ $12.50 = $25.00 — porque cada método y campo lleva el nombre de su propósito. No fue necesario ningún comentario para seguir la lógica del carrito; los nombres con intención reveladora se encargaron de la documentación.
  • El subtotal es $30.97 y el envío es $5.99, no gratuito: la cláusula de guarda en shippingCents comparó el subtotal con la constante nombrada FREE_SHIPPING_THRESHOLD_CENTS (5000), y 3097 está por debajo. El número mágico vive en exactamente un lugar, por lo que la regla no puede volverse inconsistente.
  • value equality: true demuestra que el record nos dio equals por valor de forma gratuita — dos elementos Pen construidos por separado son iguales porque sus datos son iguales. Escribir ese par equals/hashCode a mano es código repetitivo que ya no tienes que mantener.
  • rejected bad item: quantity must be >= 0 muestra el principio de fallar rápido en acción: el constructor compacto validó el argumento y lanzó IllegalArgumentException en el momento de la construcción, por lo que una cantidad de -1 nunca puede entrar al sistema como dato incorrecto silencioso.
  • Cada auxiliar — subtotalCents, shippingCents, formatCents — hace una sola cosa, por lo que main se lee de arriba a abajo como una historia. Los métodos pequeños con responsabilidad única son lo que hace que todo el programa sea escaneable en lugar de una pared de lógica anidada.

Práctica

Práctica
¿Por qué se considera más limpio hacer de 'LineItem' un record con validación en su constructor compacto en lugar de una clase mutable ordinaria con setters?
¿Por qué se considera más limpio hacer de 'LineItem' un record con validación en su constructor compacto en lugar de una clase mutable ordinaria con setters?
Was this page helpful?