Serie 0x5 - Estructuras de Control y Flujo¶
0x5000 - Un solo return por método [Restricción Didáctica]¶
Explicación¶
Para métodos que retornan valor (non-void), debe haber exactamente un return al final del método. Esto facilita razonar sobre el flujo y garantiza que las postcondiciones se cumplan.
Justificación¶
Flujo lineal: Más fácil seguir la lógica.
Postcondiciones: Se pueden establecer justo antes del return.
Debugging: Un solo punto para breakpoint de salida.
Claridad: Se sabe dónde termina el método.
Excepción¶
Métodos void pueden tener múltiples returns (o ninguno), porque tienen dos puntos de salida naturales: el return explícito y el fin del método.
Correcto para métodos non-void:
public int calcular(int x) {
int resultado;
if (x < 0) {
resultado = 0;
} else {
resultado = x * 2;
}
return resultado; // ✅ Un solo return al final
}0x5001 - Sin usar la asignación compuesta (+=, -=, *=, etc) [Restricción Didáctica]¶
Explicación¶
Por motivos pedagógicos, en este curso no se permite el uso de operadores de asignación compuesta (+=, -=, *=, /=, %=, etc.). En su lugar, debe usarse la forma explícita de la operación completa.
Justificación¶
Claridad pedagógica: La forma explícita hace evidente la operación completa.
Razonamiento explícito: Obliga a pensar en términos de “calcular nuevo valor y asignar”.
Menos errores: Evita confusiones sobre qué está siendo modificado.
Uniformidad: Todas las asignaciones lucen similares, facilitando el análisis.
Base conceptual: Antes de usar atajos, entender la operación completa.
Sintaxis¶
PROHIBIDO: variable OP= expresion
REQUERIDO: variable = variable OP expresion
Donde OP puede ser: +, -, *, /, %, &, |, ^, <<, >>Operadores prohibidos¶
// ❌ Todos estos están prohibidos en el curso
contador += 1; // Usar: contador = contador + 1
total -= descuento; // Usar: total = total - descuento
precio *= 1.21; // Usar: precio = precio * 1.21
dividendo /= 2; // Usar: dividendo = dividendo / 2
resto %= 10; // Usar: resto = resto % 10
// ❌ También los operadores de incremento/decremento
i++; // Usar: i = i + 1
++i; // Usar: i = i + 1
i--; // Usar: i = i - 1
--i; // Usar: i = i - 1Ejemplos correctos¶
// ✅ CORRECTO: Forma explícita
public int calcularFactorial(int n) {
int resultado = 1;
for (int i = 2; i <= n; i = i + 1) { // ✅ No i++
resultado = resultado * i; // ✅ No resultado *= i
}
return resultado;
}
public int sumarArray(int[] numeros) {
int suma = 0;
for (int i = 0; i < numeros.length; i = i + 1) { // ✅ Explícito
suma = suma + numeros[i]; // ✅ Explícito
}
return suma;
}0x5002 - Sin break y continue, en su lugar usen banderas [Restricción Didáctica]¶
Explicación¶
En este curso, no se permite el uso de las sentencias break y continue para controlar el flujo de lazos (excepto en switch). En su lugar, deben usarse variables booleanas (banderas) en la condición del lazo para controlar su terminación o continuación.
Justificación¶
Flujo explícito: Las banderas hacen explícita la condición de terminación en la declaración del lazo.
Claridad: La condición del
while/formuestra todas las razones para continuar/terminar.Razonamiento: Facilita el análisis formal y la verificación de invariantes.
Menos saltos: Evita saltos implícitos que dificultan seguir el flujo.
Pedagogía: Refuerza el pensamiento sobre condiciones de lazo antes de escribir el código.
break - Terminación anticipada¶
Problema: Uso de break ❌¶
// ❌ INCORRECTO: Usar break
public int buscarElemento(int[] arreglo, int elemento) {
int indice = -1;
for (int i = 0; i < arreglo.length; i = i + 1) {
if (arreglo[i] == elemento) {
indice = i;
break; // ❌ No permitido
}
}
return indice;
}Problemas:
La condición de terminación no está visible en la declaración del
forRequiere seguir el cuerpo del lazo para entender cuándo termina
El
breakes un salto implícito fuera del lazo
Solución: Bandera en la condición ✅¶
// ✅ CORRECTO: Usar bandera
public int buscarElemento(int[] arreglo, int elemento) {
int indice = -1;
boolean encontrado = false;
int i = 0;
while (i < arreglo.length && !encontrado) { // ✅ Condición explícita
if (arreglo[i] == elemento) {
indice = i;
encontrado = true; // ✅ Actualizar bandera
}
i = i + 1;
}
return indice;
}Ventajas:
La condición
!encontradoestá visible en la declaración del lazoEs obvio que el lazo termina cuando encuentra el elemento
No hay saltos implícitos
continue - Saltar iteración¶
Problema: Uso de continue ❌¶
// ❌ INCORRECTO: Usar continue
public int contarPositivos(int[] numeros) {
int contador = 0;
for (int i = 0; i < numeros.length; i = i + 1) {
if (numeros[i] <= 0) {
continue; // ❌ No permitido
}
contador = contador + 1;
}
return contador;
}Solución: Condicional simple ✅¶
// ✅ CORRECTO: Usar condicional
public int contarPositivos(int[] numeros) {
int contador = 0;
for (int i = 0; i < numeros.length; i = i + 1) {
if (numeros[i] > 0) { // ✅ Lógica positiva
contador = contador + 1;
}
}
return contador;
}Ejemplos comparativos¶
Ejemplo 1: Búsqueda con condición¶
// ❌ Incorrecto con break
public String buscarPrimeraPalabraLarga(String[] palabras) {
String resultado = null;
for (int i = 0; i < palabras.length; i = i + 1) {
if (palabras[i].length() > 5) {
resultado = palabras[i];
break; // ❌
}
}
return resultado;
}
// ✅ Correcto con bandera
public String buscarPrimeraPalabraLarga(String[] palabras) {
String resultado = null;
boolean encontrada = false;
int i = 0;
while (i < palabras.length && !encontrada) {
if (palabras[i].length() > 5) {
resultado = palabras[i];
encontrada = true; // ✅
}
i = i + 1;
}
return resultado;
}Ejemplo 2: Procesar elementos válidos¶
// ❌ Incorrecto con continue
public void procesarElementosValidos(int[] elementos) {
for (int i = 0; i < elementos.length; i = i + 1) {
if (elementos[i] < 0) {
continue; // ❌ Saltar elementos negativos
}
if (elementos[i] > 100) {
continue; // ❌ Saltar elementos grandes
}
procesarElemento(elementos[i]);
}
}
// ✅ Correcto con condicionales
public void procesarElementosValidos(int[] elementos) {
for (int i = 0; i < elementos.length; i = i + 1) {
boolean esValido = elementos[i] >= 0 && elementos[i] <= 100;
if (esValido) { // ✅ Condición positiva
procesarElemento(elementos[i]);
}
}
}Ejemplo 3: Búsqueda con múltiples condiciones¶
// ❌ Incorrecto con break
public boolean existeElementoEspecial(int[] arreglo) {
for (int i = 0; i < arreglo.length; i = i + 1) {
if (arreglo[i] % 2 == 0 && arreglo[i] > 10) {
return true; // ❌ Podría ser break
}
}
return false;
}
// ✅ Correcto con bandera
public boolean existeElementoEspecial(int[] arreglo) {
boolean existe = false;
int i = 0;
while (i < arreglo.length && !existe) {
if (arreglo[i] % 2 == 0 && arreglo[i] > 10) {
existe = true; // ✅ Usar bandera
}
i = i + 1;
}
return existe;
}Excepción: break en switch¶
El único contexto donde break está permitido es en sentencias switch:
// ✅ CORRECTO: break en switch está permitido
public String obtenerNombreDia(int dia) {
String nombre;
switch (dia) {
case 1:
nombre = "Lunes";
break; // ✅ Permitido en switch
case 2:
nombre = "Martes";
break; // ✅ Permitido en switch
// ...
default:
nombre = "Día inválido";
break; // ✅ Permitido en switch
}
return nombre;
}Por qué existe esta regla¶
Esta es una regla pedagógica para forzar el razonamiento sobre condiciones de lazo:
// break oculta la condición de terminación
while (true) {
if (condicion) break; // Condición oculta en el cuerpo
}
// Bandera hace explícita la condición
boolean continuar = true;
while (continuar) { // Condición visible
if (condicion) {
continuar = false;
}
}El Costo de las Banderas y el Entorno Profesional¶
Es importante reconocer el costo de usar banderas: introducen variables adicionales de estado, aumentan la verbosidad y a veces pueden volver el código menos directo o menos legible si la lógica es simple.
Sin embargo, la prohibición de break y continue en esta instancia se usa solo como entrenamiento didáctico. Su objetivo es forzar la capacidad de modelar mentalmente la condición de terminación real (el invariante y la condición de corte completa) antes de escribir el cuerpo del lazo, evitando que se usen saltos implícitos como “parches” para salir de lazos mal diseñados. Una vez internalizado este razonamiento, en código profesional el uso de retornos tempranos y break/continue es considerado estándar y a menudo preferible por ser menos verboso.
| Aspecto | break/continue | Banderas |
|---|---|---|
| Claridad de condición | ⚠️ Oculta (en el cuerpo) | ✅ Explícita (en la firma) |
| Facilidad de lectura | ⚠️ Requiere análisis línea a línea | ✅ Visible arriba |
| Código profesional | ✅ Estándar y aceptado | ⚠️ Verboso (costo de estado adicional) |
| En este curso (Entrenamiento) | ❌ Prohibido | ✅ Requerido |
Patrón común: Búsqueda con terminación anticipada¶
// ✅ Patrón estándar para búsquedas
public int buscar(int[] arreglo, int elemento) {
int resultado = -1;
boolean encontrado = false;
int i = 0;
while (i < arreglo.length && !encontrado) {
if (arreglo[i] == elemento) {
resultado = i;
encontrado = true;
}
i = i + 1;
}
return resultado;
}Patrón común: Saltar elementos inválidos¶
// ✅ Patrón estándar para filtrado
public int contarValidos(String[] elementos) {
int contador = 0;
for (int i = 0; i < elementos.length; i = i + 1) {
boolean esValido = elementos[i] != null && elementos[i].length() > 0;
if (esValido) {
contador = contador + 1;
}
}
return contador;
}0x5003 - Usar parámetros como variables solo si no cambia su significado [Criterio de Diseño]¶
Explicación¶
Los parámetros de un método representan las entradas originales y no deben modificarse para almacenar valores intermedios o resultados. Si necesitás un valor derivado o transformado, creá una variable local nueva con un nombre descriptivo.
Justificación¶
Claridad semántica: El parámetro siempre mantiene su significado original durante todo el método.
Debugging: Al inspeccionar el parámetro, siempre ves el valor original recibido.
Razonamiento: Facilita verificar precondiciones y razonar sobre el código.
Legibilidad: Las variables locales con nombres distintos indican transformaciones.
Inmutabilidad conceptual: Los parámetros actúan como valores de entrada inmutables.
Anti-patrón: Modificar parámetro ❌¶
// ❌ INCORRECTO: Modificar el parámetro
public int calcularDobleConDescuento(int precio) {
precio = precio * 2; // ❌ 'precio' ya no es el precio original
precio = precio - 10; // ❌ Sigue modificando 'precio'
return precio;
}Problemas:
En línea 2,
precioya no significa “precio recibido”Dificulta debugging (el valor original se pierde)
No es claro qué representa
precioen cada punto
Solución correcta: Variable local ✅¶
// ✅ CORRECTO: Usar variable local para transformaciones
public int calcularDobleConDescuento(int precio) {
int precioDoble = precio * 2;
int precioFinal = precioDoble - 10;
return precioFinal;
}
// ✅ O más compacto si no necesitás intermedios
public int calcularDobleConDescuento(int precio) {
int precioFinal = (precio * 2) - 10;
return precioFinal;
}Ejemplos comparativos¶
Ejemplo 1: Transformación de número¶
// ❌ Incorrecto: Reutilizar parámetro
public int procesarNumero(int numero) {
numero = numero + 10; // ❌ Perdimos el valor original
numero = numero * 2; // ❌ 'numero' tiene otro significado
numero = numero - 5; // ❌ Y otro más
return numero;
}
// ✅ Correcto: Variables locales descriptivas
public int procesarNumero(int numero) {
int numeroIncrementado = numero + 10;
int numeroDoble = numeroIncrementado * 2;
int resultado = numeroDoble - 5;
return resultado;
}
// ✅ O con una sola variable resultado
public int procesarNumero(int numero) {
int resultado = numero + 10;
resultado = resultado * 2;
resultado = resultado - 5;
return resultado;
}Ejemplo 2: Cálculo con múltiples pasos¶
// ❌ Incorrecto: Modificar parámetros
public double calcularPrecioFinal(double precioBase, double descuento) {
precioBase = precioBase - descuento; // ❌ Ya no es el precio base
precioBase = precioBase * 1.21; // ❌ Ahora tiene IVA incluido
return precioBase;
}
// ✅ Correcto: Variables intermedias
public double calcularPrecioFinal(double precioBase, double descuento) {
double precioConDescuento = precioBase - descuento;
double precioConIva = precioConDescuento * 1.21;
return precioConIva;
}Ejemplo 3: Procesamiento de texto¶
// ❌ Incorrecto: Modificar parámetro String
public String normalizarTexto(String texto) {
texto = texto.trim(); // ❌ Perdimos el texto original
texto = texto.toLowerCase(); // ❌ 'texto' cambió de significado
texto = texto.replace(" ", "_"); // ❌ Y sigue cambiando
return texto;
}
// ✅ Correcto: Variable para resultado
public String normalizarTexto(String texto) {
String textoSinEspacios = texto.trim();
String textoMinusculas = textoSinEspacios.toLowerCase();
String textoNormalizado = textoMinusculas.replace(" ", "_");
return textoNormalizado;
}
// ✅ O con una variable resultado
public String normalizarTexto(String texto) {
String resultado = texto.trim();
resultado = resultado.toLowerCase();
resultado = resultado.replace(" ", "_");
return resultado;
}Caso especial: Parámetros en lazos¶
Los parámetros tampoco deben usarse como contadores o índices:
// ❌ Incorrecto: Usar parámetro como contador
public void imprimirN Veces(String mensaje, int veces) {
while (veces > 0) {
System.out.println(mensaje);
veces = veces - 1; // ❌ 'veces' ya no es la cantidad original
}
}
// ✅ Correcto: Variable local para contador
public void imprimirNVeces(String mensaje, int veces) {
int contador = 0;
while (contador < veces) {
System.out.println(mensaje);
contador = contador + 1;
}
}
// ✅ O con variable para restantes
public void imprimirNVeces(String mensaje, int veces) {
int restantes = veces; // Variable local copia el valor
while (restantes > 0) {
System.out.println(mensaje);
restantes = restantes - 1;
}
}Cuando NO aplicar la regla¶
La regla dice “si no cambia su significado”. Hay situaciones donde usar el parámetro directamente es aceptable:
Validación y normalización de entrada¶
// ✅ Aceptable: normalizar entrada al inicio
public String procesarTexto(String texto) {
// Normalizar entrada: texto sigue siendo "el texto de entrada"
if (texto == null) {
texto = ""; // ✅ Aceptable: establece valor por defecto
}
// Ahora trabajar con texto
int longitud = texto.length();
return analizarTexto(texto);
}Parámetros que no se vuelven a usar¶
// ✅ Aceptable si el parámetro no se usa después
public int calcular(int valor) {
if (valor < 0) {
valor = 0; // ✅ Solo se usa para retornar inmediatamente
return valor;
}
return valor * 2;
}
// ⚠️ Pero mejor usar variable local para claridad
public int calcular(int valor) {
int valorNormalizado = (valor < 0) ? 0 : valor;
return valorNormalizado * 2;
}Beneficio: Depuración más fácil¶
// ❌ Difícil de depurar
public int calcular(int numero) {
numero = numero + 5; // Breakpoint aquí: numero = valor_original + 5
numero = numero * 2; // Breakpoint aquí: numero = (valor_original + 5) * 2
numero = numero - 3; // Breakpoint aquí: ¿cuál es el valor original?
return numero;
}
// ✅ Fácil de depurar
public int calcular(int numero) {
int conIncremento = numero + 5; // Breakpoint: numero = valor_original
int duplicado = conIncremento * 2; // Breakpoint: ambos visibles
int resultado = duplicado - 3; // Breakpoint: todos visibles
return resultado;
}Parámetros de objetos mutables¶
La regla también aplica a objetos:
// ❌ Incorrecto: Modificar parámetro objeto
public void procesarPedido(Pedido pedido) {
pedido = new Pedido(); // ❌ Ahora perdimos la referencia original
pedido.setTotal(100.0);
}
// ✅ Correcto: Crear nuevo objeto con variable local
public Pedido crearCopia(Pedido pedidoOriginal) {
Pedido pedidoCopia = new Pedido();
pedidoCopia.setId(pedidoOriginal.getId());
pedidoCopia.setTotal(pedidoOriginal.getTotal());
return pedidoCopia;
}Resumen¶
// ❌ Incorrecto: Parámetro cambia de significado
public int metodo(int valor) {
valor = valor + 1; // 'valor' ya no es el valor recibido
valor = valor * 2; // 'valor' tiene otro significado
return valor;
}
// ✅ Correcto: Parámetro mantiene su significado
public int metodo(int valor) {
int resultado = valor + 1; // 'valor' sigue siendo el valor recibido
resultado = resultado * 2; // 'resultado' es claro
return resultado;
}0x5004 - Separación de Responsabilidades: No mezclar I/O (printf, Scanner) con Lógica de Negocio [Criterio de Diseño]¶
Explicación¶
Esta regla no es una prohibición puramente formal del uso de bibliotecas de consola, sino un criterio arquitectónico fundamental de separación de responsabilidades (Separation of Concerns). Los métodos de lógica de negocio no deben mezclar cálculos o procesamiento con operaciones de entrada/salida (I/O). El uso de System.out.println(), printf(), Scanner, o cualquier otra forma de I/O debe estar restringido a métodos cuyo propósito explícito sea la capa de presentación o de interacción.
Justificación¶
Separación de responsabilidades: Lógica de negocio separada de presentación.
Testabilidad: Métodos sin I/O son más fáciles de testear automáticamente.
Reutilización: Métodos sin I/O pueden usarse en diferentes contextos (GUI, web, consola).
Mantenimiento: Cambiar la forma de presentación no requiere modificar la lógica.
Arquitectura limpia: Base para arquitecturas en capas bien diseñadas.
Principio: Separación de Capas¶
Anti-patrón: I/O mezclado con lógica ❌¶
// ❌ INCORRECTO: Mezclar cálculo con impresión
public double calcularPromedio(double[] valores) {
double suma = 0.0;
System.out.println("Calculando promedio..."); // ❌ I/O en lógica
for (int i = 0; i < valores.length; i = i + 1) {
suma = suma + valores[i];
System.out.println("Suma parcial: " + suma); // ❌ I/O en lógica
}
double promedio = suma / valores.length;
System.out.println("Promedio: " + promedio); // ❌ I/O en lógica
return promedio;
}Problemas:
No podés testear el método sin producir output en consola
No podés usar el método en GUI o web sin modificarlo
El método hace dos cosas: calcular Y mostrar
Solución correcta: Separar responsabilidades ✅¶
// ✅ CORRECTO: Método de lógica pura
public double calcularPromedio(double[] valores) {
double suma = 0.0;
for (int i = 0; i < valores.length; i = i + 1) {
suma = suma + valores[i];
}
return suma / valores.length; // Solo retorna, no imprime
}
// ✅ CORRECTO: Método separado para presentación
public void mostrarCalculoPromedio(double[] valores) {
System.out.println("Calculando promedio...");
double promedio = calcularPromedio(valores); // Usa lógica
System.out.println("Promedio: " + promedio);
}Ejemplo completo: Separación correcta¶
Capa de lógica (sin I/O)¶
public class CalculadoraDescuentos {
/**
* Calcula el precio final aplicando descuento según tipo de cliente.
*
* @param precioBase Precio original del producto
* @param tipoCliente Tipo de cliente (REGULAR, PREMIUM, VIP)
* @return Precio final con descuento aplicado
*/
public double calcularPrecioConDescuento(double precioBase,
TipoCliente tipoCliente) {
double porcentajeDescuento = obtenerPorcentajeDescuento(tipoCliente);
double descuento = precioBase * porcentajeDescuento;
double precioFinal = precioBase - descuento;
return precioFinal; // ✅ Solo retorna, no imprime
}
private double obtenerPorcentajeDescuento(TipoCliente tipo) {
double porcentaje;
switch (tipo) {
case REGULAR:
porcentaje = 0.0;
break;
case PREMIUM:
porcentaje = 0.10;
break;
case VIP:
porcentaje = 0.20;
break;
default:
porcentaje = 0.0;
break;
}
return porcentaje; // ✅ Solo retorna
}
}Capa de presentación (con I/O)¶
public class InterfazUsuario {
private CalculadoraDescuentos calculadora;
private Scanner scanner;
public InterfazUsuario() {
this.calculadora = new CalculadoraDescuentos();
this.scanner = new Scanner(System.in);
}
/**
* Interactúa con el usuario para calcular precio con descuento.
* Este método SÍ puede usar Scanner y printf porque su propósito
* explícito es la interacción con el usuario.
*/
public void ejecutarCalculoInteractivo() {
// ✅ I/O permitido aquí - es el propósito del método
System.out.println("=== Calculadora de Descuentos ===");
System.out.print("Ingrese precio base: ");
double precio = scanner.nextDouble();
System.out.print("Tipo de cliente (1=Regular, 2=Premium, 3=VIP): ");
int opcion = scanner.nextInt();
TipoCliente tipo = convertirOpcionATipo(opcion);
// Delegar cálculo a capa de lógica
double precioFinal = calculadora.calcularPrecioConDescuento(precio, tipo);
// ✅ Mostrar resultado
System.out.printf("Precio original: $%.2f%n", precio);
System.out.printf("Precio con descuento: $%.2f%n", precioFinal);
System.out.printf("Ahorro: $%.2f%n", precio - precioFinal);
}
private TipoCliente convertirOpcionATipo(int opcion) {
TipoCliente tipo;
switch (opcion) {
case 2:
tipo = TipoCliente.PREMIUM;
break;
case 3:
tipo = TipoCliente.VIP;
break;
default:
tipo = TipoCliente.REGULAR;
break;
}
return tipo;
}
}Consecuencias de la separación¶
Testabilidad mejorada¶
// ✅ Método sin I/O es fácil de testear
@Test
void testCalcularPrecioConDescuento_ClientePremium_Aplica10Porciento() {
CalculadoraDescuentos calc = new CalculadoraDescuentos();
double precioFinal = calc.calcularPrecioConDescuento(100.0,
TipoCliente.PREMIUM);
assertEquals(90.0, precioFinal, 0.01); // Sin output en consola
}Reutilización en diferentes contextos¶
// ✅ Misma lógica, diferente UI
public class InterfazGrafica extends JFrame {
private CalculadoraDescuentos calculadora;
private void onCalcularButtonClick() {
double precio = Double.parseDouble(precioTextField.getText());
TipoCliente tipo = (TipoCliente) tipoComboBox.getSelectedItem();
// Reutilizamos la lógica
double precioFinal = calculadora.calcularPrecioConDescuento(precio, tipo);
resultadoLabel.setText(String.format("Precio final: $%.2f", precioFinal));
}
}Métodos donde I/O SÍ está permitido¶
Métodos cuyo propósito es I/O¶
// ✅ CORRECTO: El propósito del método ES la interacción
public class MenuPrincipal {
/**
* Muestra el menú y captura la opción del usuario.
* @return Opción seleccionada por el usuario
*/
public int mostrarMenuYCapturarOpcion() {
// ✅ I/O permitido - es el propósito del método
System.out.println("=== Menú Principal ===");
System.out.println("1. Agregar producto");
System.out.println("2. Eliminar producto");
System.out.println("3. Listar productos");
System.out.println("0. Salir");
System.out.print("Seleccione opción: ");
Scanner scanner = new Scanner(System.in);
return scanner.nextInt();
}
}Método main¶
// ✅ CORRECTO: main puede tener I/O
public class Aplicacion {
public static void main(String[] args) {
// ✅ main puede interactuar con usuario
System.out.println("Bienvenido al sistema");
InterfazUsuario interfaz = new InterfazUsuario();
interfaz.iniciar(); // Delegar a clase responsable de I/O
System.out.println("Gracias por usar el sistema");
}
}Violaciones comunes en el curso¶
Violación 1: Debug prints en lógica ❌¶
// ❌ INCORRECTO
public int buscarMaximo(int[] numeros) {
int maximo = numeros[0];
System.out.println("Buscando máximo..."); // ❌ Debug print
for (int i = 1; i < numeros.length; i = i + 1) {
if (numeros[i] > maximo) {
maximo = numeros[i];
System.out.println("Nuevo máximo: " + maximo); // ❌
}
}
return maximo;
}
// ✅ CORRECTO: Sin I/O
public int buscarMaximo(int[] numeros) {
int maximo = numeros[0];
for (int i = 1; i < numeros.length; i = i + 1) {
if (numeros[i] > maximo) {
maximo = numeros[i];
}
}
return maximo;
}Violación 2: Pedir datos dentro de método de cálculo ❌¶
// ❌ INCORRECTO: Pedir input en método de cálculo
public double calcularAreaRectangulo() {
Scanner scanner = new Scanner(System.in);
System.out.print("Ingrese base: "); // ❌ I/O en lógica
double base = scanner.nextDouble();
System.out.print("Ingrese altura: "); // ❌ I/O en lógica
double altura = scanner.nextDouble();
return base * altura;
}
// ✅ CORRECTO: Parámetros en lugar de input
public double calcularAreaRectangulo(double base, double altura) {
return base * altura; // Lógica pura
}
// ✅ Método separado para interacción
public void calcularAreaInteractivo() {
Scanner scanner = new Scanner(System.in);
System.out.print("Ingrese base: ");
double base = scanner.nextDouble();
System.out.print("Ingrese altura: ");
double altura = scanner.nextDouble();
double area = calcularAreaRectangulo(base, altura); // Usa lógica
System.out.printf("El área es: %.2f%n", area);
}Patrón recomendado: Controlador¶
// ✅ Patrón: Separar en tres responsabilidades
public class SistemaFacturacion {
// 1. Lógica de negocio (sin I/O)
public class CalculadorFactura {
public double calcularTotal(List<Producto> productos) {
double total = 0.0;
for (Producto p : productos) {
total = total + p.getPrecio();
}
return total;
}
}
// 2. Entrada de datos (con I/O)
public class LectorDatos {
private Scanner scanner = new Scanner(System.in);
public List<Producto> leerProductos() {
System.out.print("¿Cuántos productos? ");
int cantidad = scanner.nextInt();
List<Producto> productos = new ArrayList<>();
for (int i = 0; i < cantidad; i = i + 1) {
productos.add(leerProducto());
}
return productos;
}
private Producto leerProducto() {
System.out.print("Nombre: ");
String nombre = scanner.next();
System.out.print("Precio: ");
double precio = scanner.nextDouble();
return new Producto(nombre, precio);
}
}
// 3. Presentación de resultados (con I/O)
public class MostradorResultados {
public void mostrarFactura(List<Producto> productos, double total) {
System.out.println("\n=== FACTURA ===");
for (Producto p : productos) {
System.out.printf("%s: $%.2f%n", p.getNombre(), p.getPrecio());
}
System.out.printf("TOTAL: $%.2f%n", total);
}
}
// 4. Coordinador (orquesta las tres capas)
public static void main(String[] args) {
LectorDatos lector = new LectorDatos();
CalculadorFactura calculador = new CalculadorFactura();
MostradorResultados mostrador = new MostradorResultados();
List<Producto> productos = lector.leerProductos();
double total = calculador.calcularTotal(productos);
mostrador.mostrarFactura(productos, total);
}
}Métodos donde I/O SÍ es permitido¶
Clase de interfaz de usuario¶
// ✅ CORRECTO: Clase dedicada a I/O
public class InterfazConsola {
private Scanner scanner;
public InterfazConsola() {
this.scanner = new Scanner(System.in);
}
/**
* Lee un número entero del usuario.
* El propósito explícito del método ES leer input.
*/
public int leerEntero(String mensaje) {
System.out.print(mensaje);
return scanner.nextInt();
}
/**
* Muestra un mensaje al usuario.
* El propósito explícito del método ES mostrar output.
*/
public void mostrarMensaje(String mensaje) {
System.out.println(mensaje);
}
/**
* Muestra un resultado formateado.
*/
public void mostrarResultado(String etiqueta, double valor) {
System.out.printf("%s: %.2f%n", etiqueta, valor);
}
}Testing: Por qué importa¶
// ❌ Método con I/O - difícil de testear
public int calcularYMostrar(int a, int b) {
int resultado = a + b;
System.out.println("Resultado: " + resultado); // ❌
return resultado;
}
// Test problemático
@Test
void testCalcular() {
// Problema: genera output en consola durante test
int resultado = calcularYMostrar(2, 3);
assertEquals(5, resultado);
// Output: "Resultado: 5" aparece en el log de tests
}// ✅ Método sin I/O - fácil de testear
public int calcular(int a, int b) {
return a + b; // ✅ Lógica pura
}
// Test limpio
@Test
void testCalcular_ConDosNumeros_RetornaSuma() {
int resultado = calcular(2, 3);
assertEquals(5, resultado); // Sin output espurio
}Inyección de dependencias para I/O avanzado¶
Para casos más avanzados, inyectar la fuente de I/O:
// ✅ Diseño avanzado: I/O inyectable
public class ProcesadorDatos {
private Scanner scanner;
private PrintStream output;
// Constructor con dependencias inyectadas
public ProcesadorDatos(Scanner input, PrintStream output) {
this.scanner = input;
this.output = output;
}
// Método que necesita I/O pero es testeable
public void procesarDatosInteractivo() {
output.print("Ingrese dato: ");
int dato = scanner.nextInt();
int resultado = procesarDato(dato); // Lógica separada
output.printf("Resultado: %d%n", resultado);
}
// Lógica pura, sin I/O
private int procesarDato(int dato) {
return dato * 2; // ✅ Testeable fácilmente
}
}
// Test con mocks
@Test
void testProcesarDatosInteractivo() {
String input = "5\n";
ByteArrayInputStream in = new ByteArrayInputStream(input.getBytes());
ByteArrayOutputStream out = new ByteArrayOutputStream();
ProcesadorDatos proc = new ProcesadorDatos(
new Scanner(in),
new PrintStream(out)
);
proc.procesarDatosInteractivo();
assertTrue(out.toString().contains("Resultado: 10"));
}Resumen visual¶
// ❌ Mal diseño: Todo mezclado
public class Calculadora {
public double calcular() {
System.out.print("Ingrese número: "); // ❌ Input
double num = new Scanner(System.in).nextDouble();
double resultado = num * 2;
System.out.println("Resultado: " + resultado); // ❌ Output
return resultado;
}
}
// ✅ Buen diseño: Responsabilidades separadas
public class Calculadora {
// Solo lógica, sin I/O
public double calcular(double numero) {
return numero * 2;
}
}
public class UI {
private Calculadora calc = new Calculadora();
private Scanner scanner = new Scanner(System.in);
// Solo I/O y coordinación
public void ejecutar() {
System.out.print("Ingrese número: ");
double num = scanner.nextDouble();
double resultado = calc.calcular(num); // Usa lógica
System.out.println("Resultado: " + resultado);
}
}0x5005 - Evitar anidamiento profundo de condicionales (máximo 3 niveles) [Criterio de Diseño]¶
Explicación¶
El anidamiento excesivo de estructuras condicionales (if, else, switch) dificulta significativamente la lectura y comprensión del código. Esta regla establece un máximo de 3 niveles de profundidad de anidación. Cuando se necesita más, debe refactorizarse usando retornos tempranos (guard clauses) o extracción de métodos.
Justificación¶
Complejidad ciclomática: Reduce la complejidad cognitiva del código.
Legibilidad: Código profundamente anidado es difícil de seguir.
Mantenibilidad: Más fácil agregar o modificar condiciones.
Prevención de errores: Menos niveles reducen errores de lógica.
Principio de un solo nivel de abstracción: Cada método debe operar a un nivel conceptual.
Límites de anidamiento¶
NIVEL 1: if (condicion1) { ✅ Permitido
NIVEL 2: if (condicion2) { ✅ Permitido
NIVEL 3: if (condicion3) { ✅ Permitido (máximo)
NIVEL 4: if (condicion4) { ❌ PROHIBIDO
// ...
}
}
}
}Anti-patrón: Anidamiento profundo ❌¶
// ❌ INCORRECTO: 5 niveles de anidamiento
public void procesarPedido(Pedido pedido) {
if (pedido != null) { // Nivel 1
if (pedido.esValido()) { // Nivel 2
if (pedido.getCliente() != null) { // Nivel 3
if (pedido.tieneSaldo()) { // Nivel 4 ❌
if (pedido.tieneStock()) { // Nivel 5 ❌
// Lógica profundamente anidada
procesarPagoYEnvio(pedido);
}
}
}
}
}
}Problemas:
Muy difícil de leer
La lógica principal está profundamente enterrada
Dificulta agregar nuevas condiciones
Complejidad ciclomática alta
Solución 1: Guard Clauses (Retorno temprano) ✅¶
// ✅ CORRECTO: Guard clauses - sin anidamiento
public void procesarPedido(Pedido pedido) {
// Validaciones tempranas con retorno
if (pedido == null) {
return;
}
if (!pedido.esValido()) {
return;
}
if (pedido.getCliente() == null) {
return;
}
if (!pedido.tieneSaldo()) {
return;
}
if (!pedido.tieneStock()) {
return;
}
// Lógica principal a nivel superior - fácil de leer
procesarPagoYEnvio(pedido);
}Ventajas:
La lógica principal está al nivel superior
Fácil agregar nuevas validaciones
Cada condición es independiente y clara
Cero niveles de anidamiento
Solución 2: Extracción de métodos ✅¶
// ✅ CORRECTO: Extraer validaciones a métodos
public void procesarPedido(Pedido pedido) {
if (!esProcesamientoValido(pedido)) { // Método auxiliar
return;
}
// Lógica principal
procesarPagoYEnvio(pedido);
}
private boolean esProcesamientoValido(Pedido pedido) {
if (pedido == null) {
return false;
}
if (!pedido.esValido()) {
return false;
}
if (pedido.getCliente() == null) {
return false;
}
if (!pedido.tieneSaldo()) {
return false;
}
if (!pedido.tieneStock()) {
return false;
}
return true;
}Solución 3: Combinar condiciones ✅¶
// ✅ CORRECTO: Condiciones compuestas cuando tiene sentido
public void procesarPedido(Pedido pedido) {
boolean pedidoEsValido = pedido != null
&& pedido.esValido()
&& pedido.getCliente() != null;
boolean tieneTodo = pedido.tieneSaldo() && pedido.tieneStock();
if (!pedidoEsValido || !tieneTodo) {
return;
}
// Lógica principal
procesarPagoYEnvio(pedido);
}Ejemplos de diferentes contextos¶
Ejemplo 1: Validación de datos¶
// ❌ Incorrecto: Anidamiento profundo
public boolean validarUsuario(Usuario usuario) {
if (usuario != null) {
if (usuario.getNombre() != null) {
if (usuario.getNombre().length() > 0) {
if (usuario.getEmail() != null) {
if (usuario.getEmail().contains("@")) {
return true;
}
}
}
}
}
return false;
}
// ✅ Correcto: Guard clauses
public boolean validarUsuario(Usuario usuario) {
if (usuario == null) {
return false;
}
if (usuario.getNombre() == null || usuario.getNombre().length() == 0) {
return false;
}
if (usuario.getEmail() == null || !usuario.getEmail().contains("@")) {
return false;
}
return true;
}Ejemplo 2: Procesamiento con múltiples condiciones¶
// ❌ Incorrecto: Anidamiento excesivo
public double calcularDescuento(Cliente cliente, double monto) {
if (cliente != null) {
if (cliente.esPremium()) {
if (monto > 1000) {
if (cliente.getMesesAnti guedad() > 12) {
return monto * 0.25; // 25% descuento
} else {
return monto * 0.15; // 15% descuento
}
} else {
return monto * 0.10; // 10% descuento
}
} else {
if (monto > 500) {
return monto * 0.05;
}
}
}
return 0.0;
}
// ✅ Correcto: Guard clauses y método auxiliar
public double calcularDescuento(Cliente cliente, double monto) {
if (cliente == null) {
return 0.0;
}
if (cliente.esPremium()) {
return calcularDescuentoPremium(monto, cliente.getMesesAntiguedad());
} else {
return calcularDescuentoRegular(monto);
}
}
private double calcularDescuentoPremium(double monto, int mesesAntiguedad) {
if (monto > 1000 && mesesAntiguedad > 12) {
return monto * 0.25;
} else if (monto > 1000) {
return monto * 0.15;
} else {
return monto * 0.10;
}
}
private double calcularDescuentoRegular(double monto) {
if (monto > 500) {
return monto * 0.05;
}
return 0.0;
}Medición de niveles de anidamiento¶
public void metodo() {
// Nivel 0 (método)
if (condicion1) { // → Nivel 1
// código
if (condicion2) { // → Nivel 2
// código
if (condicion3) { // → Nivel 3 (máximo permitido)
// código
}
}
}
}Anidamiento con diferentes estructuras¶
La regla aplica a cualquier estructura de control:
// ❌ Mezcla de if, for, while anidados
public void procesar(List<Pedido> pedidos) {
for (Pedido p : pedidos) { // Nivel 1
if (p.esValido()) { // Nivel 2
for (Item i : p.getItems()) { // Nivel 3
if (i.tieneStock()) { // Nivel 4 ❌
// ...
}
}
}
}
}
// ✅ Extraer método interno
public void procesar(List<Pedido> pedidos) {
for (Pedido p : pedidos) { // Nivel 1
if (p.esValido()) { // Nivel 2
procesarItemsPedido(p); // Nivel 3 → método
}
}
}
private void procesarItemsPedido(Pedido pedido) {
for (Item i : pedido.getItems()) { // Nivel 1 (en nuevo método)
if (i.tieneStock()) { // Nivel 2
procesarItem(i);
}
}
}Técnica: Inversión de condicionales¶
// ❌ Incorrecto: Anidamiento innecesario
public void procesar(String dato) {
if (dato != null) {
if (dato.length() > 0) {
if (dato.startsWith("A")) {
// procesar
}
}
}
}
// ✅ Correcto: Invertir y retornar temprano
public void procesar(String dato) {
if (dato == null) return;
if (dato.length() == 0) return;
if (!dato.startsWith("A")) return;
// procesar
}
// ✅ O combinar condiciones
public void procesar(String dato) {
boolean esValido = dato != null
&& dato.length() > 0
&& dato.startsWith("A");
if (!esValido) return;
// procesar
}Patrón: Polimorfismo para eliminar anidamiento¶
// ❌ Incorrecto: Switch anidado
public double calcularPrecio(Producto producto) {
if (producto.getCategoria() == Categoria.ELECTRONICA) {
if (producto.esNuevo()) {
if (producto.tieneGarantia()) {
return producto.getPrecioBase() * 1.3;
} else {
return producto.getPrecioBase() * 1.1;
}
}
}
// ... más casos
return producto.getPrecioBase();
}
// ✅ Correcto: Polimorfismo
public interface EstrategiaPrecio {
double calcular(Producto producto);
}
public class PrecioElectronicaNueva implements EstrategiaPrecio {
public double calcular(Producto producto) {
if (producto.tieneGarantia()) {
return producto.getPrecioBase() * 1.3;
} else {
return producto.getPrecioBase() * 1.1;
}
}
}0x5006 - Los lazos deben tener condiciones de terminación claras [Criterio de Diseño]¶
Explicación¶
Todo lazo debe tener una condición de terminación clara, comprensible y garantizada. La condición debe hacer obvio por qué y cuándo el lazo terminará. Evitar condiciones complejas que requieran análisis profundo para entender el comportamiento del lazo.
Justificación¶
Prevención de lazos infinitos: Condiciones claras reducen el riesgo de lazos sin fin.
Legibilidad: Debe ser obvio cuándo termina el lazo sin analizar el cuerpo.
Razonamiento: Facilita demostrar que el lazo termina (terminación garantizada).
Mantenimiento: Condiciones simples son más fáciles de modificar correctamente.
Debugging: Más fácil identificar por qué un lazo no termina cuando debería.
Características de una buena condición¶
CONDICIÓN CLARA debe ser:
- Evaluable sin leer el cuerpo del lazo
- Compuesta por máximo 2-3 subexpresiones
- Con nombres de variables descriptivos
- Sin lógica compleja o llamadas a métodos pesadosEjemplos de condiciones claras ✅¶
// ✅ Condición simple: contador hasta límite
for (int i = 0; i < numeros.length; i = i + 1) {
// Es obvio: termina cuando i alcanza length
}
// ✅ Condición con bandera: búsqueda
boolean encontrado = false;
int i = 0;
while (i < elementos.length && !encontrado) {
// Es obvio: termina cuando encuentra o recorre todo
}
// ✅ Condición doble: ventana deslizante
int inicio = 0;
int fin = 0;
while (fin < datos.length && inicio <= fin) {
// Relación clara entre inicio y fin
}
// ✅ Condición con sentinel
int indice = 0;
while (arreglo[indice] != VALOR_SENTINEL) {
// Es obvio: termina al encontrar sentinel
}Anti-patrón: Condiciones complejas ❌¶
// ❌ INCORRECTO: Demasiadas condiciones
boolean flag1 = false;
boolean flag2 = true;
boolean flag3 = false;
int contador = 0;
int limite = 100;
while (flag2 && !flag1 && contador < limite && !flag3 && otraCondicion()) {
// ❌ No es claro cuándo termina
// ❌ Demasiadas variables involucradas
// ❌ Llamada a método en condición
}Problemas:
5 subexpresiones en la condición
Variables con nombres genéricos (
flag1,flag2)Llamada a método en cada iteración
No es obvio qué combinación causa la terminación
Solución: Simplificar y nombrar bien ✅¶
// ✅ CORRECTO: Condición simplificada
boolean elementoEncontrado = false;
int indice = 0;
int cantidadElementos = elementos.size();
while (!elementoEncontrado && indice < cantidadElementos) {
if (elementos.get(indice).cumpleCondicion()) {
elementoEncontrado = true;
}
indice = indice + 1;
}Patrón: Búsqueda lineal¶
// ✅ Patrón estándar de búsqueda
public int buscar(int[] arreglo, int elemento) {
boolean encontrado = false;
int indice = 0;
// Condición clara: busca mientras no encuentre y haya elementos
while (indice < arreglo.length && !encontrado) {
if (arreglo[indice] == elemento) {
encontrado = true;
} else {
indice = indice + 1;
}
}
if (encontrado) {
return indice;
} else {
return -1;
}
}Patrón: Procesamiento hasta condición¶
// ✅ Patrón: procesar hasta cumplir criterio
public int procesarHastaLimite(int[] valores, int limite) {
int suma = 0;
int indice = 0;
boolean limiteSuperado = false;
// Condición clara: mientras haya elementos y no supere límite
while (indice < valores.length && !limiteSuperado) {
suma = suma + valores[indice];
if (suma >= limite) {
limiteSuperado = true;
}
indice = indice + 1;
}
return suma;
}Garantizar terminación¶
Lazos contados (siempre terminan)¶
// ✅ Terminación garantizada: contador hasta límite fijo
for (int i = 0; i < 10; i = i + 1) {
// Terminación garantizada: i eventualmente alcanza 10
}
// ✅ Terminación garantizada: recorrer arreglo
for (int i = 0; i < arreglo.length; i = i + 1) {
// Terminación garantizada: i eventualmente alcanza length
}Lazos con progreso garantizado¶
// ✅ Progreso garantizado hacia terminación
int valor = 100;
while (valor > 0) {
valor = valor - 10; // Decrece en cada iteración → terminará
}
// ✅ Progreso garantizado: consumir lista
List<String> items = obtenerItems();
while (!items.isEmpty()) {
String item = items.remove(0); // Lista se reduce → terminará
procesarItem(item);
}Anti-patrón: Terminación no garantizada ❌¶
// ❌ PELIGRO: Posible lazo infinito
int valor = 10;
while (valor != 0) {
valor = valor + 2; // ❌ Nunca será 0, incrementa de a 2
}
// ❌ PELIGRO: Depende de factor externo
while (!archivo.estaListo()) {
// ❌ ¿Qué pasa si el archivo nunca está listo?
esperar(100);
}
// ❌ PELIGRO: Condición que podría no cambiar
boolean condicion = calcularCondicion();
while (condicion) {
procesarDatos();
// ❌ ¿condicion se actualiza dentro del lazo?
}Soluciones para casos problemáticos¶
Agregar timeout o límite de iteraciones¶
// ✅ Seguro: Límite de intentos
int intentos = 0;
int MAX_INTENTOS = 100;
boolean condicion = calcularCondicion();
while (condicion && intentos < MAX_INTENTOS) {
procesarDatos();
condicion = calcularCondicion();
intentos = intentos + 1;
}
if (intentos >= MAX_INTENTOS) {
throw new RuntimeException("Máximo de intentos alcanzado");
}Actualización explícita de condición¶
// ✅ Actualización clara de la condición
boolean continuarProcesando = true;
int elementosProcesados = 0;
while (continuarProcesando && elementosProcesados < elementos.size()) {
Elemento elem = elementos.get(elementosProcesados);
boolean exitoso = procesarElemento(elem);
if (!exitoso) {
continuarProcesando = false; // Actualización explícita
}
elementosProcesados = elementosProcesados + 1;
}Lazos while vs for¶
Usar for cuando el número de iteraciones es conocido¶
// ✅ for cuando sabés cuántas iteraciones
for (int i = 0; i < 10; i = i + 1) {
// Exactamente 10 iteraciones
}
for (int i = 0; i < arreglo.length; i = i + 1) {
// Exactamente length iteraciones
}Usar while cuando depende de condición¶
// ✅ while cuando depende de condición dinámica
boolean encontrado = false;
int i = 0;
while (i < arreglo.length && !encontrado) {
// Termina cuando encuentra O termina el arreglo
}
// ✅ while para entrada de usuario
Scanner scanner = new Scanner(System.in);
int opcion = -1;
while (opcion != 0) {
opcion = scanner.nextInt();
// Termina cuando usuario ingresa 0
}Documentar condiciones complejas¶
Si la condición es compleja pero necesaria, documentarla:
// ✅ Condición compleja pero documentada
/**
* Procesa elementos hasta que:
* - Se procesan todos los elementos, O
* - Se alcanza el límite de peso, O
* - Se encuentra un elemento inválido
*/
public void procesarElementos(List<Elemento> elementos, double limiteP eso) {
boolean limiteSuperado = false;
boolean elementoInvalido = false;
int indice = 0;
double pesoAcumulado = 0.0;
// Condición: tres formas de terminar claramente documentadas
while (indice < elementos.size()
&& !limiteSuperado
&& !elementoInvalido) {
Elemento elem = elementos.get(indice);
if (!elem.esValido()) {
elementoInvalido = true;
} else {
pesoAcumulado = pesoAcumulado + elem.getPeso();
if (pesoAcumulado > limitePeso) {
limiteSuperado = true;
}
}
indice = indice + 1;
}
}Resumen¶
// ❌ Condición confusa
while (f1 && !f2 && c < L && m() && !f3) {
// ¿Cuándo termina?
}
// ✅ Condición clara
boolean encontrado = false;
int indice = 0;
while (indice < elementos.length && !encontrado) {
// Obvio: termina cuando encuentra o recorre todo
}indice = indice + 1;}