Clean Code en Java
¿Qué es Clean Code?
Código que es fácil de leer, entender y mantener. No se trata de que funcione, sino de que sea comprensible.
Principios SOLID
Single Responsibility (Responsabilidad única)
Una clase debe tener una sola razón para cambiar.
// ❌ Hace demasiadopublic class Usuario { public void guardarEnBaseDatos() { } public void enviarEmail() { } public void generarReporte() { }}
// ✅ Una responsabilidad por clasepublic class Usuario { private String nombre; private String email;}
public class UsuarioRepositorio { public void guardar(Usuario usuario) { }}
public class EmailServicio { public void enviar(Usuario usuario, String mensaje) { }}Open/Closed (Abierto/Cerrado)
Abierto para extensión, cerrado para modificación.
// ✅ Extensible sin modificar código existentepublic interface FiguraGeometrica { double calcularArea();}
public class Circulo implements FiguraGeometrica { private double radio;
@Override public double calcularArea() { return Math.PI * radio * radio; }}
// Agregar nueva figura sin modificar código existentepublic class Cuadrado implements FiguraGeometrica { private double lado;
@Override public double calcularArea() { return lado * lado; }}Liskov Substitution (Sustitución de Liskov)
Los subtipos deben poder reemplazar a sus tipos base.
// ❌ Viola LSPpublic class Ave { public void volar() { }}
public class Pinguino extends Ave { @Override public void volar() { throw new UnsupportedOperationException(); // ❌ }}
// ✅ Diseño correctopublic interface Ave { }
public interface AveVoladora extends Ave { void volar();}
public class Aguila implements AveVoladora { @Override public void volar() { }}
public class Pinguino implements Ave { // No implementa volar}Interface Segregation (Segregación de interfaces)
Interfaces específicas son mejores que una general.
// ❌ Interfaz grandepublic interface Trabajador { void trabajar(); void comer(); void dormir();}
// ✅ Interfaces pequeñas y específicaspublic interface Trabajable { void trabajar();}
public interface Alimentable { void comer();}
public class Empleado implements Trabajable, Alimentable { @Override public void trabajar() { }
@Override public void comer() { }}Dependency Inversion (Inversión de dependencias)
Depende de abstracciones, no de concreciones.
// ❌ Depende de implementación concretapublic class UsuarioServicio { private MySQLUsuarioRepositorio repositorio = new MySQLUsuarioRepositorio();}
// ✅ Depende de abstracciónpublic interface UsuarioRepositorio { Usuario buscar(Long id);}
public class UsuarioServicio { private final UsuarioRepositorio repositorio;
public UsuarioServicio(UsuarioRepositorio repositorio) { this.repositorio = repositorio; }}Nombres significativos
Variables
// ❌ Nombres crípticosint d;List<String> lst1;
// ✅ Nombres descriptivosint diasDesdeCreacion;List<String> usuariosActivos;Métodos
// ❌ Vagopublic void procesar() { }public void hacer() { }
// ✅ Específicopublic void validarDatosUsuario() { }public void enviarNotificacionEmail() { }Funciones pequeñas
Una función debe hacer una cosa y hacerla bien.
// ❌ Función larga que hace muchas cosaspublic void procesarPedido(Pedido pedido) { // Validar pedido if (pedido == null) throw new IllegalArgumentException(); if (pedido.getItems().isEmpty()) throw new IllegalArgumentException();
// Calcular total double total = 0; for (Item item : pedido.getItems()) { total += item.getPrecio() * item.getCantidad(); } pedido.setTotal(total);
// Aplicar descuento if (pedido.getCliente().esPremium()) { total *= 0.9; }
// Guardar en base de datos repositorio.guardar(pedido);
// Enviar email emailServicio.enviar(pedido.getCliente().getEmail(), "Pedido procesado");}
// ✅ Funciones pequeñas y específicaspublic void procesarPedido(Pedido pedido) { validarPedido(pedido); calcularTotal(pedido); guardarPedido(pedido); notificarCliente(pedido);}
private void validarPedido(Pedido pedido) { Objects.requireNonNull(pedido, "Pedido no puede ser null"); if (pedido.getItems().isEmpty()) { throw new IllegalArgumentException("Pedido sin items"); }}
private void calcularTotal(Pedido pedido) { double total = pedido.getItems().stream() .mapToDouble(item -> item.getPrecio() * item.getCantidad()) .sum();
if (pedido.getCliente().esPremium()) { total *= 0.9; }
pedido.setTotal(total);}DRY (Don't Repeat Yourself)
No repitas código.
// ❌ Duplicaciónpublic double calcularPrecioA(double precio) { double impuesto = precio * 0.21; return precio + impuesto;}
public double calcularPrecioB(double precio) { double impuesto = precio * 0.21; return precio + impuesto;}
// ✅ Sin duplicaciónpublic double calcularPrecioConImpuesto(double precio) { double impuesto = precio * 0.21; return precio + impuesto;}Evita comentarios obvios
El código debe ser autoexplicativo.
// ❌ Comentarios innecesarios// Obtiene el nombre del usuariopublic String getNombre() { return nombre; // Retorna nombre}
// ✅ Código claro sin comentariospublic String getNombre() { return nombre;}Comenta el por qué, no el qué:
// ✅ Explica decisiones no obvias// Usamos un timeout de 30 segundos porque la API externa// puede tardar hasta 25 segundos en responder bajo cargapublic static final int TIMEOUT_SEGUNDOS = 30;Manejo de errores
Usa excepciones, no códigos de error
// ❌ Códigos de errorpublic int dividir(int a, int b) { if (b == 0) return -1; // ¿-1 es error o resultado válido? return a / b;}
// ✅ Excepcionespublic int dividir(int a, int b) { if (b == 0) { throw new IllegalArgumentException("No se puede dividir por cero"); } return a / b;}No retornes null
// ❌ Retorna nullpublic Usuario buscarUsuario(Long id) { // ... return null; // Obliga al llamador a verificar null}
// ✅ Usa Optionalpublic Optional<Usuario> buscarUsuario(Long id) { // ... return Optional.ofNullable(usuario);}Inmutabilidad
Objetos inmutables son más seguros.
// ✅ Clase inmutablepublic final class Punto { private final int x; private final int y;
public Punto(int x, int y) { this.x = x; this.y = y; }
public int getX() { return x; } public int getY() { return y; }
// Para "modificar", retorna nuevo objeto public Punto mover(int dx, int dy) { return new Punto(x + dx, y + dy); }}
// O usa recordspublic record Punto(int x, int y) { public Punto mover(int dx, int dy) { return new Punto(x + dx, y + dy); }}Evita números mágicos
// ❌ Números mágicosif (usuario.getEdad() > 18) { // ...}
// ✅ Constantes con nombreprivate static final int EDAD_MINIMA = 18;
if (usuario.getEdad() > EDAD_MINIMA) { // ...}Composición sobre herencia
// ❌ Herencia para reutilizaciónpublic class Stack extends ArrayList { public void push(Object value) { add(value); }}
// ✅ Composiciónpublic class Stack { private List<Object> items = new ArrayList<>();
public void push(Object value) { items.add(value); }
public Object pop() { return items.remove(items.size() - 1); }}Tests como documentación
El código de test documenta cómo usar tu código.
@Testpublic void deberiaCalcularDescuentoPremium() { Usuario usuario = new Usuario("Juan", true); // premium Pedido pedido = new Pedido(100.0);
double total = pedido.calcularTotal(usuario);
assertEquals(90.0, total); // 10% de descuento}Próximo paso
Aprende a escribir tests efectivos: Introducción al testing →