
Aprender a programar no consiste solo en hacer que un programa funcione.
Un programa puede funcionar… y ser difícil de entender, mantener o ampliar. Por eso, además de aprender sintaxis, es fundamental adoptar una serie de buenas prácticas que nos ayuden a escribir mejor código.
Muchas de estas reglas no son nuevas: aparecen una y otra vez en libros clásicos como:
- Clean Code — Robert C. Martin
- Effective Java — Joshua Bloch
- Refactoring — Martin Fowler
- The Pragmatic Programmer — Andrew Hunt y David Thomas
- Code Complete — Steve McConnell
A continuación se presentan 12 reglas fundamentales, con ejemplos en Java.
1. Usa nombres que expresen intención
👉 Un buen nombre debe indicar claramente qué representa o qué hace la variable, el método o la clase, sin necesidad de comentarios.
📚 Referencia: Clean Code
🔸 Variables
❌ Mal:
int x;
✔️ Bien:
int edad;
🔸 Métodos
❌ Mal:
void f() {
// ...
}
✔️ Bien:
void calcularMedia() {
// ...
}
🔸 Clases
❌ Mal:
class Datos {
}
✔️ Bien:
class Alumno {
}
📎 Si necesitas un comentario para explicar qué hace una variable o un método, probablemente su nombre no es adecuado.
2. Las funciones deben ser pequeñas y hacer una sola cosa
👉 Una función debe tener una única responsabilidad y ser fácil de entender de un vistazo.
📚 Referencias: “Clean Code“, “Refactoring“.
📌 Relacionado con: SRP (Single Responsibility Principle)
🔸 Ejemplo
❌ Mal:
void procesarAlumno(Alumno alumno) {
// validar edad
if (alumno.getEdad() < 0) {
System.out.println("Edad incorrecta");
}
// calcular media de notas
double media = (alumno.getNota1() + alumno.getNota2()) / 2;
// guardar en fichero
System.out.println("Guardando alumno...");
}
✔️ Bien:
void procesarAlumno(Alumno alumno) {
validarAlumno(alumno);
double media = calcularMedia(alumno);
guardarAlumno(alumno, media);
}
void validarAlumno(Alumno alumno) {
if (alumno.getEdad() < 0) {
System.out.println("Edad incorrecta");
}
}
double calcularMedia(Alumno alumno) {
return (alumno.getNota1() + alumno.getNota2()) / 2;
}
void guardarAlumno(Alumno alumno, double media) {
System.out.println("Guardando alumno...");
}
📎 Refactorización
El paso de la versión “mal” a la versión “bien” no consiste en añadir funcionalidad, sino en mejorar la estructura del código sin cambiar su comportamiento.
Este proceso se conoce como refactorización (refactoring). En este caso concreto, hemos aplicado la técnica de extracción de métodos (extract method), dividiendo una función grande en varias funciones más pequeñas y especializadas.
⚠️ Señales de alerta
- Si el nombre de la función contiene “y” (por ejemplo,
calcularYGuardar) → probablemente hace más de una cosa - Si necesitas poner comentarios dentro de una función → probablemente deberías dividirla en varias funciones más pequeñas
- Si una función es difícil de explicar en una sola frase → probablemente hace demasiadas cosas
3. Evita comentarios innecesarios
👉 El código debe ser lo suficientemente claro como para no necesitar comentarios que expliquen qué hace.
📚 Referencia: Clean Code
🔸 Ejemplo con variables
❌ Mal:
// Edad
int x;
✔️ Bien:
int edad;
En el primer caso, el comentario es necesario porque el nombre de la variable no aporta información. En el segundo, el propio nombre hace innecesaria cualquier explicación.
🔸 Ejemplo con funciones
❌ Mal:
void procesarAlumno(Alumno alumno) {
// validar edad
if (alumno.getEdad() < 0) {
System.out.println("Edad incorrecta");
}
// calcular media
double media = (alumno.getNota1() + alumno.getNota2()) / 2;
// guardar alumno
System.out.println("Guardando alumno...");
}
✔️ Bien:
void procesarAlumno(Alumno alumno) {
validarAlumno(alumno);
double media = calcularMedia(alumno);
guardarAlumno(alumno, media);
}
En el primer caso, los comentarios son necesarios porque el código no es claro. En el segundo, los nombres de los métodos explican exactamente qué ocurre, por lo que los comentarios dejan de ser necesarios.
⚠️ Nota importante
Esto no significa que los comentarios sean malos.
Son útiles para:
- explicar decisiones de diseño
- documentar APIs
- aclarar aspectos no evidentes
Pero no deberían usarse para explicar código que podría ser más claro.
Regla práctica
- Si un comentario explica qué hace el código → probablemente el código puede mejorar.
- Si un comentario explica por qué se hace algo → suele ser útil.
4. No repitas código (DRY)
👉 El principio DRY (Don’t Repeat Yourself, «no te repitas») establece que cada pieza de lógica debe tener una única representación en el código. Cuando el mismo cálculo o bloque de instrucciones aparece copiado en varios sitios, cualquier corrección futura obliga a localizar y modificar todas las copias; si se pasa por alto alguna, el programa queda en un estado inconsistente.
📚 Referencia: The Pragmatic Programmer
📌 Acrónimo: DRY (Don’t Repeat Yourself)
🔸 Ejemplo
❌ Mal:
public class Main {
public static void main(String[] args) {
double radio1 = 5.0;
double area1 = 3.14 * radio1 * radio1;
System.out.printf("Area del circulo 1: %.2f%n", area1);
double radio2 = 3.0;
double area2 = 3.14 * radio2 * radio2;
System.out.printf("Area del circulo 2: %.2f%n", area2);
double radio3 = 7.5;
double area3 = 3.14 * radio3 * radio3;
System.out.printf("Area del circulo 3: %.2f%n", area3);
}
}
En este código, el cálculo del área está duplicado en varios sitios. Si en el futuro se decide aumentar la precisión de π o añadir una comprobación sobre el radio, habría que modificar todas las apariciones.
✔️ Mejor:
public class Main {
public static void main(String[] args) {
double radio1 = 5.0;
double area1 = calcularAreaCirculo(radio1);
System.out.printf("Area del circulo 1: %.2f%n", area1);
double radio2 = 3.0;
double area2 = calcularAreaCirculo(radio2);
System.out.printf("Area del circulo 2: %.2f%n", area2);
double radio3 = 7.5;
double area3 = calcularAreaCirculo(radio3);
System.out.printf("Area del circulo 3: %.2f%n", area3);
}
static double calcularAreaCirculo(double radio) {
return Math.PI * radio * radio;
}
}
Ahora, el cálculo está centralizado en un único método. Cualquier cambio se realiza en un solo lugar, lo que reduce errores y facilita el mantenimiento.
Aún así, en main() se repiten tres bloques casi idénticos. Podríamos usar un bucle y simplificar el código:
✔️ Bien:
public class Main {
public static void main(String[] args) {
double[] radios = {5.0, 3.0, 7.5};
for(int i=0; i<radios.length; i++) {
double area = calcularAreaCirculo(radios[i]);
System.out.printf("Area del circulo %d: %.2f%n",
i+1, area);
}
}
static double calcularAreaCirculo(double radio) {
return Math.PI * radio * radio;
}
}
👉 Regla práctica
- Si repites un mismo cálculo o una misma fórmula → probablemente estás violando DRY
- Si un cambio obliga a modificar varios sitios → hay duplicación
- DRY no solo evita repetir código, sino evitar repetir conocimiento
⚠️ Nota importante
En Java, muchas constantes de uso habitual ya están definidas en la biblioteca estándar. Por ejemplo, el número π está disponible como Math.PI, con toda la precisión necesaria. Siempre que sea posible, es preferible usar constantes ya definidas en lugar de redefinirlas.
5. Evita los números mágicos
👉 Los valores constantes deben tener un nombre significativo que explique qué representan y aparecer en el código a través de la constante que los representa, no codificados directamente como números mágicos.
📚 Referencia: Clean Code
🔸 Ejemplo
❌ Mal:
static double calcularPrecioConIva(double precio) {
return precio * 1.21;
}
static double calcularIva(double precio) {
return precio * 0.21;
}
En este código, el valor 0.21 aparece repetido en varias funciones. Si el tipo de IVA cambia, hay que buscar y modificar todas las apariciones, con el riesgo de pasar alguna por alto. Además, alguien que lea el código por primera vez no sabe inmediatamente qué significa ese valor.
✔️ Bien:
static final double IVA = 0.21;
static double calcularPrecioConIva(double precio) {
return precio * (1 + IVA);
}
static double calcularIva(double precio) {
return precio * IVA;
}
Ahora el valor tiene un nombre que explica su significado, y aparece en un único lugar. Si el tipo impositivo cambia, la modificación se hace una sola vez y queda actualizada en todas las partes del programa que lo utilizan.
Regla práctica
- Si aparece un número «suelto» en el código → probablemente debería ser una constante
- Si no sabes qué significa un valor sin leer más código → necesita un nombre
- Las constantes hacen el código más claro y más fácil de mantener
6. Evita listas largas de parámetros
👉 Cuando una función tiene muchos parámetros, suele ser difícil de entender y utilizar correctamente.
📚 Referencia: Effective Java
No hay un número mágico, pero como regla general:
- 1–2 parámetros → normal
- 3 parámetros → empieza a ser sospechoso
- 4 o más → probablemente hay un problema de diseño
🔸 Ejemplo
❌ Mal:
void imprimirUsuario(String nombre, int edad, String email, String telefono) {
System.out.println(nombre + " (" + edad + ")");
...
}
✔️ Bien:
class Usuario {
String nombre;
int edad;
String email;
String telefono;
}
void imprimirUsuario(Usuario usuario) {
System.out.println(usuario.nombre + " (" + usuario.edad + ")");
...
}
En el primer caso, la función recibe muchos datos independientes, lo que la hace más difícil de usar y más propensa a errores (por ejemplo, cambiar el orden de los parámetros). En el segundo, los datos están agrupados en un objeto que representa un concepto del dominio (Usuario), lo que hace el código más claro y más fácil de manejar.
Por ahora los campos de Usuario son accesibles directamente desde fuera de la clase. En la siguiente regla veremos por qué eso puede ser un problema y cómo solucionarlo.
Regla práctica
- Si una función tiene muchos parámetros → revisa si puedes agruparlos en un objeto
- Si varios parámetros están relacionados → probablemente deberían formar una clase
- Si dudas sobre el orden de los parámetros → el diseño puede mejorarse
🔥 Nota avanzada
👉 En algunos casos, cuando es necesario crear objetos con muchos parámetros, se utilizan técnicas como el patrón Builder, que se estudiarán en asignaturas más avanzadas.
7. Prefiere objetos inmutables
👉 Un objeto inmutable es aquel cuyo estado no puede cambiar después de crearse.
📚 Referencia: Effective Java
🔸 Ejemplo
❌ Mal:
class Usuario {
String nombre;
int edad;
}
Usuario u = new Usuario();
u.nombre = "Ana";
u.edad = 20;
u.edad = -5; // nadie lo impide
En este código, los campos son accesibles directamente desde fuera de la clase. Cualquier parte del programa puede modificar el estado del objeto en cualquier momento, incluso asignando valores inválidos.
✔️ Bien:
class Usuario {
private final String nombre;
private final int edad;
public Usuario(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
public String getNombre() { return nombre; }
public int getEdad() { return edad; }
}
Ahora los campos son privados y no pueden modificarse tras la construcción. El objeto se crea de una vez con todos sus datos y su estado no cambia.
En la regla anterior agrupábamos datos en un objeto (Usuario). Aquí damos un paso más: hacemos que ese objeto sea seguro y consistente.
Regla práctica
- Si un objeto no necesita cambiar → hazlo inmutable
- Si un atributo no debe modificarse → decláralo
final - Evita exponer atributos directamente → usa encapsulación
⚠️ Nota importante
La inmutabilidad aporta varias ventajas:
- Evita errores difíciles de detectar
- Facilita el razonamiento sobre el código
- Hace el código más seguro en entornos concurrentes
8. Maneja correctamente los errores
👉 Los errores no deben ignorarse: deben detectarse y tratarse de forma adecuada.
📚 Referencia: Effective Java
🔸 Ejemplo
En la regla anterior hicimos que Usuario fuera inmutable. Pero aún es posible construir un objeto con valores inválidos:
❌ Mal:
class Usuario {
private final String nombre;
private final int edad;
public Usuario(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad; // no se valida
}
}
Usuario u = new Usuario("Ana", -5); // se crea sin error
El objeto se construye sin problemas, aunque la edad sea negativa. El error pasa desapercibido.
✔️ Bien:
class Usuario {
private final String nombre;
private final int edad;
public Usuario(String nombre, int edad) {
if (edad < 0) {
throw new IllegalArgumentException("La edad no puede ser negativa");
}
this.nombre = nombre;
this.edad = edad;
}
public String getNombre() { return nombre; }
public int getEdad() { return edad; }
}
Ahora el error se detecta en el momento adecuado y se comunica mediante una excepción. Es imposible construir un Usuario con un estado inválido.
🔸 Ejemplo de uso
❌ Mal:
try {
Usuario u = new Usuario("Ana", -5);
} catch (Exception e) {
// ignorar el error
}
✔️ Bien:
try {
Usuario u = new Usuario("Ana", -5);
} catch (IllegalArgumentException e) {
System.out.println("Error al crear el usuario: " + e.getMessage());
}
En el primer caso, el error se captura pero se ignora, lo que puede ocultar problemas graves. En el segundo, se gestiona de forma explícita.
Regla práctica
- No ignores excepciones → siempre haz algo con ellas
- Usa tipos de excepción específicos (
IllegalArgumentException, etc.) - Valida los datos lo antes posible, preferiblemente en el constructor
⚠️ Nota importante
Lanzar una excepción no es «fallar», es:
- detectar un problema antes de que se propague
- evitar que el objeto quede en un estado incorrecto
- hacer el error visible en lugar de ocultarlo
Conexión con la regla anterior
En la regla anterior hicimos el objeto
Usuarioinmutable: sus campos no pueden cambiar tras la construcción. Aquí añadimos validación para garantizar que el objeto nunca se construya en un estado inválido. Las dos reglas juntas aseguran queUsuariosea siempre correcto.
9. Escribe código fácil de leer (KISS)
👉 La solución más simple y clara suele ser la mejor.
📚 Referencia: The Pragmatic Programmer
📌 Acrónimo: KISS (Keep It Simple, Stupid)
🔸 Ejemplo
❌ Mal:
if ((edad >= 18 && edad <= 65 && !tieneDeudas) || esEmpleadoVIP) {
permitirAcceso();
}
✔️ Bien:
boolean edadValida = edad >= 18 && edad <= 65;
boolean clienteSinDeudas = !tieneDeudas;
boolean accesoEspecial = esEmpleadoVIP;
if ((edadValida && clienteSinDeudas) || accesoEspecial) {
permitirAcceso();
}
En el primer caso, la condición es difícil de leer y entender de un vistazo. En el segundo, el uso de variables intermedias permite expresar claramente la intención del código.
Regla práctica
- Si una expresión es difícil de leer → divídela
- Usa variables con nombres significativos
- Prefiere claridad frente a “código compacto”
⚠️ Nota importante
Escribir menos código no siempre significa escribir mejor código. A veces, añadir unas pocas líneas mejora mucho la legibilidad.
Conexión con reglas anteriores
Igual que en reglas anteriores, los nombres (edadValida, clienteSinDeudas) hacen que el código se explique por sí mismo, evitando la necesidad de comentarios.
10. Escribe tests siempre que puedas
👉 Un test permite comprobar automáticamente que una parte del programa funciona como esperamos.
📚 Referencia: Test Driven Development: By Example
📌 Relacionado con: TDD (Test-Driven Development)
🔸 Ejemplo
Supongamos que queremos implementar un método que determine si un alumno está aprobado.
❌ Sin test:
class Evaluacion {
static boolean estaAprobado(double nota) {
return nota > 5; // error sutil
}
}
✔️ Con test:
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import org.junit.jupiter.api.Test;
class EvaluacionTest {
@Test
void aprobadoConCinco() {
assertTrue(Evaluacion.estaAprobado(5));
}
@Test
void suspensoConMenosDeCinco() {
assertFalse(Evaluacion.estaAprobado(4.9));
}
}
En este caso, el código tiene un error (> 5 en lugar de >= 5) que es fácil de cometer y difícil de detectar a simple vista. Al ejecutar el test y ver que falla, podemos localizar enseguida el error. El test define claramente el comportamiento esperado y permite detectar el error de forma inmediata.
class Evaluacion {
static boolean estaAprobado(double nota) {
return nota >= 5;
}
}
Regla práctica
- Si una regla tiene límites (≥, ≤, etc.) → es importante testearla
- Los tests ayudan a detectar errores pequeños pero importantes
- Un buen test describe claramente el comportamiento esperado
⚠️ Nota importante
Los tests son especialmente útiles cuando:
- Hay condiciones límite.
- Hay reglas del dominio (como “aprobado”).
- El código puede cambiar en el futuro.
Conexión con reglas anteriores
Igual que en la regla DRY, aquí también aparece una regla del sistema (“qué es aprobar”). Los tests permiten asegurar que esa regla se cumple siempre.
11. Refactoriza constantemente
👉 Mejorar el código es parte del desarrollo, no una fase posterior.
📚 Referencia: Refactoring
🔸 Ejemplo
Supongamos que ya tenemos un código que funciona correctamente:
class Evaluacion {
static boolean estaAprobado(double nota) {
return nota >= 5;
}
static String mensajeResultado(double nota) {
if (nota >= 5) {
return "Aprobado";
} else {
return "Suspenso";
}
}
}
El código funciona, pero hay duplicación.
✔️ Refactorizado:
class Evaluacion {
static boolean estaAprobado(double nota) {
return nota >= 5;
}
static String mensajeResultado(double nota) {
if (estaAprobado(nota)) {
return "Aprobado";
} else {
return "Suspenso";
}
}
}
El código inicial es correcto, pero repite la misma lógica (nota >= 5) en varios sitios. Al refactorizar, reutilizamos el método estaAprobado(), haciendo el código más claro y evitando duplicación.
Regla práctica
- Refactorizar no es “arreglar errores”, sino mejorar el código
- Si ves duplicación → refactoriza
- Si algo se puede expresar mejor → refactoriza
⚠️ Nota importante
Refactorizar significa:
- No cambiar el comportamiento.
- Mejorar la estructura interna.
- Facilitar futuras modificaciones.
Conexión con reglas anteriores
En esta regla se aplican varias ideas vistas antes:
- DRY → eliminamos duplicación
- nombres claros →
estaAprobado - funciones pequeñas → mejor organización
📎 Un buen programador no solo escribe código que funciona, sino que mejora continuamente el código que ya funciona.
12. Piensa antes de programar
👉 Antes de escribir código, es fundamental entender el problema y diseñar una solución.
📚 Referencia: The Pragmatic Programmer
🔸 Ejemplo
Supongamos que queremos gestionar los datos de varios alumnos y determinar si han aprobado. Sin reflexionar sobre el diseño, es habitual empezar así:
❌ Mal:
String[] nombres = {"Ana", "Luis", "María"};
double[] notas = {8.5, 4.0, 9.0};
for (int i = 0; i < nombres.length; i++) {
if (notas[i] >= 5) {
System.out.println(nombres[i] + ": Aprobado");
} else {
System.out.println(nombres[i] + ": Suspenso");
}
}
El código funciona para este caso concreto, pero los datos de un alumno están repartidos en arrays distintos. Si añadimos un atributo nuevo (por ejemplo, el email), necesitamos un tercer array y recordar mantener siempre los tres sincronizados. La relación entre los datos no está expresada en el código.
✔️ Bien:
class Alumno {
String nombre;
double nota;
public Alumno(String nombre, double nota) {
this.nombre = nombre;
this.nota = nota;
}
boolean estaAprobado() {
return nota >= 5;
}
}
...
Alumno[] alumnos = {
new Alumno("Ana", 8.5),
new Alumno("Luis", 4.0),
new Alumno("María", 9.0)
};
for (Alumno a : alumnos) {
String resultado = a.estaAprobado() ? "Aprobado" : "Suspenso";
System.out.println(a.nombre + ": " + resultado);
}
Al modelar el problema antes de codificarlo, los datos de cada alumno están encapsulados en un objeto y la lógica de negocio («¿está aprobado?») vive junto a los datos a los que pertenece. Si mañana necesitamos guardar también el email de cada alumno, en la versión con arrays tendríamos que añadir un tercer array y mantenerlo sincronizado con los otros dos. En la versión con clase, basta con añadir un campo a Alumno y el resto del código no se ve afectado.
En un programa real aplicaríamos también las reglas 7 y 8: haríamos los campos privados y validaríamos los datos en el constructor.
Regla práctica
- Antes de programar → identifica los conceptos del problema
- Agrupa en una clase los datos que representan una misma entidad
- La lógica que pertenece a un dato debe estar cerca de ese dato
⚠️ Nota importante
Muchos problemas de programación no son de sintaxis, sino de diseño. Dedicar unos minutos a pensar qué entidades intervienen y cómo se relacionan suele simplificar mucho el código resultante.
Conexión con todo el artículo
Esta regla resume todas las anteriores:
- Nombres claros → pensar qué representan los datos
- Funciones pequeñas → diseñar responsabilidades
- DRY → identificar conocimiento común
- Inmutabilidad → decidir qué debe cambiar
Todas las buenas prácticas empiezan antes de escribir código.
📎 Programar bien no consiste solo en escribir código que funcione, sino en construir soluciones que otros puedan entender, mantener y mejorar. Y todo ello empieza mucho antes de escribir la primera línea de código.
📊 Resumen de reglas
| # | Regla | Acrónimo | Referencia |
|---|---|---|---|
| 1 | Usa nombres que expresen intención | — | Clean Code |
| 2 | Funciones pequeñas y con una sola responsabilidad | SRP | Clean Code, Refactoring |
| 3 | Evita comentarios innecesarios | — | Clean Code |
| 4 | No repitas código | DRY | The Pragmatic Programmer |
| 5 | Evita números mágicos | — | Clean Code |
| 6 | Evita listas largas de parámetros | — | Effective Java |
| 7 | Prefiere objetos inmutables | — | Effective Java |
| 8 | Maneja correctamente los errores | — | Effective Java |
| 9 | Escribe código simple y legible | KISS | The Pragmatic Programmer |
| 10 | Escribe tests | TDD | Test Driven Development: By example |
| 11 | Refactoriza constantemente | — | Refactoring |
| 12 | Piensa antes de programar | — | The Pragmatic Programmer |
Estas 12 reglas resumen algunos de los principios más importantes que aparecen una y otra vez en la práctica profesional.
📎 Estas reglas no son leyes estrictas, pero sí principios que aparecen constantemente en la práctica profesional. No deben memorizarse de forma aislada. En la práctica, suelen aparecer juntas y se refuerzan unas a otras. Aprenderlas desde el principio marca una gran diferencia:
- el código funciona,
- se entiende
- y puede evolucionar
Programar bien no es aplicar reglas de forma mecánica, sino desarrollar el criterio para saber cuándo y cómo aplicarlas. Porque, al final, programar bien no es solo resolver problemas, sino resolverlos de forma que otros puedan entender, mantener y continuar tu trabajo.
Santiago Higuera. 16 de abril de 2026.