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ón | Evitar | Preferir |
|---|---|---|
| Valor ausente | return null; | Optional<T> o lanzar excepción |
| Argumento incorrecto | return -1; | throw new IllegalArgumentException(...) |
| Estado imposible | valor predeterminado silencioso | throw new IllegalStateException(...) |
| Limpieza de recursos | finally manual | try-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.
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.97y el envío es$5.99, no gratuito: la cláusula de guarda enshippingCentscomparó el subtotal con la constante nombradaFREE_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: truedemuestra que elrecordnos dioequalspor valor de forma gratuita — dos elementosPenconstruidos por separado son iguales porque sus datos son iguales. Escribir ese parequals/hashCodea mano es código repetitivo que ya no tienes que mantener.rejected bad item: quantity must be >= 0muestra el principio de fallar rápido en acción: el constructor compacto validó el argumento y lanzóIllegalArgumentExceptionen el momento de la construcción, por lo que una cantidad de-1nunca puede entrar al sistema como dato incorrecto silencioso.- Cada auxiliar —
subtotalCents,shippingCents,formatCents— hace una sola cosa, por lo quemainse 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.