Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Simplificación de Condicionales

Técnicas para clarificar y simplificar lógica condicional compleja

Universidad Nacional de Río Negro

Introducción

Los condicionales son una parte fundamental de la programación, pero también son una fuente común de complejidad y errores. Un código con lógica condicional compleja es difícil de leer, mantener y testear. La refactorización de condicionales busca hacer la lógica más clara, expresiva y mantenible.

Este apunte presenta técnicas sistemáticas para simplificar expresiones booleanas, reducir anidamiento, y hacer que las condiciones sean auto-explicativas.

Reglas de estilo que resuelve

Las técnicas de este apunte no son recetas aisladas: cada una responde a un problema concreto que la cátedra modeló como regla. Detectar el antipatrón y saber qué reescritura lo elimina es lo que conecta la teoría con el código que revisás. La siguiente tabla mapea cada regla con el problema que modela y la técnica de esta guía que lo resuelve.

ReglaProblema que modelaCómo lo resuelve esta refactorización
0x1004h: Las condiciones complejas deben simplificarse o comentarseCondiciones que combinan varios operadores lógicos sin descomponerseSe extraen a variables booleanas y funciones predicado con nombre significativo
0x1013h: Detector de expresiones booleanas complejas sin paréntesis aclaratoriosExpresiones que mezclan && y `
0x1005h: Reemplazá las condiciones ambiguas basadas en la ‘veracidad’ (truthiness) del tipo de datoCondiciones que dependen de la “veracidad” implícita de enteros, punteros o charSe comparan de forma explícita contra NULL, 0, '\0' o true/false
0x100Bh: No utilices comparaciones en estilo Yoda (‘CONST == variable’)Comparaciones en estilo Yoda (CONST == variable)Se escribe siempre variable == CONSTANTE, con la variable a la izquierda
0x1012h: Prohibición de expresiones de asignación dentro de estructuras de controlAsignaciones embebidas dentro de una estructura de controlLa asignación se separa en su propia sentencia antes del if, while o for
0x1009h: Prohibición de asignaciones simples dentro de condiciones lógicasUso del operador = simple dentro de una condición lógicaLa condición se limita a comparar (==) y la asignación se mueve afuera
0x1010h: Prohibición de comparaciones encadenadas no idiomáticas en C (a < b < c)Comparaciones encadenadas no idiomáticas (a < b < c)Se reescriben de forma explícita como a < b && b < c
0x1017h: Escribí condiciones afirmativas y directasCondiciones negativas indirectas (!(...)) difíciles de leerSe afirma el caso directo o se aísla la negación en una variable positiva
0x1015h: Limitá la profundidad de anidación a tres nivelesAnidación de más de tres nivelesGuard clauses, retornos anticipados y extracción de funciones
0x2001h: Las funciones deben usar cláusulas de guarda y retornos anticipados para reducir la anidación profundaFunciones sin validar precondiciones y con el camino feliz anidadoCláusulas de guarda al inicio y liberación centralizada de recursos
0x100Fh: Prohibición de cláusula else redundante tras sentencia de retorno anticipadoelse después de una rama que termina con return o abandonaLa rama alterna se desanida al mismo nivel del if
0x2013h: Detector de bloques else superfluos tras sentencias terminalesBloques else superfluos tras return, exit, break, continue o gotoSe elimina el else y el flujo continúa al mismo nivel

Problemas Comunes con Condicionales

1. Anidamiento Excesivo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Problemático: pirámide de la perdición
if (usuario != NULL)
{
    if (usuario->activo)
    {
        if (usuario->edad >= 18)
        {
            if (usuario->saldo > 0)
            {
                procesar_compra(usuario);
            }
            else
            {
                printf("Saldo insuficiente\n");
            }
        }
        else
        {
            printf("Usuario menor de edad\n");
        }
    }
    else
    {
        printf("Usuario inactivo\n");
    }
}
else
{
    printf("Usuario inválido\n");
}

La “pirámide de la perdición” es el síntoma clásico de 0x1015h: Limitá la profundidad de anidación a tres niveles: cada nivel adicional de if aleja el camino feliz del borde izquierdo y multiplica los casos que hay que sostener mentalmente. La respuesta de esta guía es invertir las condiciones con guard clauses, tal como prescribe 0x2001h: Las funciones deben usar cláusulas de guarda y retornos anticipados para reducir la anidación profunda.

2. Condiciones Complejas

1
2
3
4
5
6
7
// Difícil de entender de un vistazo
if ((estado == ACTIVO || estado == PENDIENTE) &&
    (tipo != TEMPORAL && tipo != PRUEBA) &&
    (saldo > 1000 || credito_disponible > 500) && !(bloqueado || suspendido))
{
    // ...
}

Esta condición viola 0x1004h: Las condiciones complejas deben simplificarse o comentarse porque mezcla demasiadas subexpresiones en un solo lugar, y agrava 0x1013h: Detector de expresiones booleanas complejas sin paréntesis aclaratorios al combinar && y || sin paréntesis que expliciten la agrupación deseada. Extraer cada grupo a una variable con nombre es la solución directa.

3. Lógica Duplicada

1
2
3
4
5
6
7
8
9
if (edad >= 18 && edad <= 65 && !jubilado)
{
    precio = PRECIO_ADULTO;
}
// Más adelante en el código...
if (edad >= 18 && edad <= 65 && !jubilado)
{
    aplicar_descuento();
}

4. Booleanos Implícitos

1
2
3
4
5
6
7
8
9
10
11
12
13
// Redundante
if (es_valido() == true)
{
    // ...
}
if (contador > 0)
{
    return true;
}
else
{
    return false;
}

Comparar un booleano contra true o false no aporta información y depende de la “veracidad” del tipo; reescribilo de forma explícita como pide 0x1005h: Reemplazá las condiciones ambiguas basadas en la ‘veracidad’ (truthiness) del tipo de dato. Tampoco inviertas el orden a la manera de Yoda (true == es_valido): la variable va siempre a la izquierda, según 0x100Bh: No utilices comparaciones en estilo Yoda (‘CONST == variable’).

Técnicas de Refactorización

1. Guardia de Cláusulas (Guard Clauses)

Invertir condiciones para manejar casos especiales temprano y reducir anidamiento.

Antes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void procesar_pedido(pedido_t *pedido)
{
    if (pedido != NULL)
    {
        if (pedido->items_count > 0)
        {
            if (pedido->cliente != NULL)
            {
                if (pedido->cliente->saldo >= pedido->total)
                {
                    // Lógica principal de procesamiento
                    realizar_cargo(pedido);
                    actualizar_inventario(pedido);
                    enviar_confirmacion(pedido);
                }
                else
                {
                    printf("Saldo insuficiente\n");
                }
            }
            else
            {
                printf("Cliente inválido\n");
            }
        }
        else
        {
            printf("Pedido vacío\n");
        }
    }
    else
    {
        printf("Pedido nulo\n");
    }
}

Después:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void procesar_pedido(pedido_t *pedido)
{
    // Guardia de cláusulas - validaciones tempranas
    if (pedido == NULL)
    {
        printf("Pedido nulo\n");
        return;
    }
    if (pedido->items_count == 0)
    {
        printf("Pedido vacío\n");
        return;
    }
    if (pedido->cliente == NULL)
    {
        printf("Cliente inválido\n");
        return;
    }
    if (pedido->cliente->saldo < pedido->total)
    {
        printf("Saldo insuficiente\n");
        return;
    }
    // Lógica principal ahora está al mismo nivel
    realizar_cargo(pedido);
    actualizar_inventario(pedido);
    enviar_confirmacion(pedido);
}

Beneficios:

Invertir la condición para abandonar temprano es exactamente lo que prescriben 0x2001h: Las funciones deben usar cláusulas de guarda y retornos anticipados para reducir la anidación profunda y 0x1015h: Limitá la profundidad de anidación a tres niveles. Además, cuando una rama termina en return, el else que la sigue sobra: hay que desanidarlo según 0x100Fh: Prohibición de cláusula else redundante tras sentencia de retorno anticipado y 0x2013h: Detector de bloques else superfluos tras sentencias terminales.

2. Extracción de Condiciones a Variables Booleanas

Antes:

if (usuario->edad >= 18 && usuario->edad <= 65 && !usuario->jubilado &&
    usuario->activo && (usuario->tipo == REGULAR || usuario->tipo == PREMIUM))
{
    aplicar_beneficio(usuario);
}

Después:

1
2
3
4
5
6
7
8
bool es_adulto_en_edad_laboral =
    usuario->edad >= 18 && usuario->edad <= 65 && !usuario->jubilado;
bool es_usuario_activo_valido =
    usuario->activo && (usuario->tipo == REGULAR || usuario->tipo == PREMIUM);
if (es_adulto_en_edad_laboral && es_usuario_activo_valido)
{
    aplicar_beneficio(usuario);
}

Beneficios:

Dar nombre a cada subexpresión es la técnica que responde a 0x1004h: Las condiciones complejas deben simplificarse o comentarse y, de paso, deja explícita la agrupación que reclama 0x1013h: Detector de expresiones booleanas complejas sin paréntesis aclaratorios.

3. Extracción de Condiciones a Funciones

Para lógica compleja o reutilizable:

Antes:

1
2
3
4
5
6
7
8
void procesar_descuento(cliente_t *cliente, double total)
{
    if ((cliente->compras_totales > 10000 && cliente->antiguedad > 365) ||
        (cliente->referidos >= 5) || (cliente->tipo == VIP && cliente->activo))
    {
        aplicar_descuento_premium(total);
    }
}

Después:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool es_cliente_fiel(const cliente_t *cliente)
{
    return cliente->compras_totales > 10000 && cliente->antiguedad > 365;
}
bool es_buen_referidor(const cliente_t *cliente)
{
    return cliente->referidos >= 5;
}
bool es_vip_activo(const cliente_t *cliente)
{
    return cliente->tipo == VIP && cliente->activo;
}
bool califica_para_descuento_premium(const cliente_t *cliente)
{
    return es_cliente_fiel(cliente) || es_buen_referidor(cliente) ||
           es_vip_activo(cliente);
}
void procesar_descuento(cliente_t *cliente, double total)
{
    if (califica_para_descuento_premium(cliente))
    {
        aplicar_descuento_premium(total);
    }
}

Beneficios:

Cada predicado encapsula una porción de la condición original, que es la forma más robusta de cumplir 0x1004h: Las condiciones complejas deben simplificarse o comentarse: en lugar de comentar una expresión enredada, la partimos en funciones cuyo nombre ya documenta la intención.

4. Simplificación de Booleanos

Antes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool es_valido(int valor)
{
    if (valor > 0 && valor < 100)
    {
        return true;
    }
    else
    {
        return false;
    }
}
// Comparación redundante
if (esta_activo() == true)
{
    // ...
}
if (contador > 0 == false)
{
    // ...
}

Después:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool es_valido(int valor)
{
    return valor > 0 && valor < 100;
}
// Uso directo del booleano
if (esta_activo())
{
    // ...
}
if (!esta_activo())
{
    // ...
}
if (contador == 0)
{
    // ...
}

El uso directo del booleano elimina la dependencia de la veracidad implícita (0x1005h: Reemplazá las condiciones ambiguas basadas en la ‘veracidad’ (truthiness) del tipo de dato) y las comparaciones redundantes contra true/false. El rango valor > 0 && valor < 100 se escribe con && explícito, nunca como 0 < valor < 100, tal como exige 0x1010h: Prohibición de comparaciones encadenadas no idiomáticas en C (a < b < c). Y cuando la condición original está negada, conviene afirmarla o aislarla en una variable con nombre positivo, según 0x1017h: Escribí condiciones afirmativas y directas.

5. Reemplazo de Condicionales con Polimorfismo (simulado en C)

Para casos donde múltiples if-else determinan comportamiento:

Antes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
double calcular_area(figura_t *figura)
{
    if (figura->tipo == CIRCULO)
    {
        return PI * figura->datos.circulo.radio * figura->datos.circulo.radio;
    }
    else if (figura->tipo == RECTANGULO)
    {
        return figura->datos.rectangulo.ancho * figura->datos.rectangulo.alto;
    }
    else if (figura->tipo == TRIANGULO)
    {
        return 0.5 * figura->datos.triangulo.base *
               figura->datos.triangulo.altura;
    }
    return 0;
}

Después (con punteros a función):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef double (*calcular_area_fn)(const void *datos);
typedef struct
{
    int tipo;
    calcular_area_fn calcular_area;
    void *datos;
} figura_t;
double calcular_area_circulo(const void *datos)
{
    const circulo_t *c = (const circulo_t *)datos;
    return PI * c->radio * c->radio;
}
double calcular_area_rectangulo(const void *datos)
{
    const rectangulo_t *r = (const rectangulo_t *)datos;
    return r->ancho * r->alto;
}
double calcular_area_triangulo(const void *datos)
{
    const triangulo_t *t = (const triangulo_t *)datos;
    return 0.5 * t->base * t->altura;
}
double calcular_area(const figura_t *figura)
{
    return figura->calcular_area(figura->datos);
}

6. Tabla de Decisión

Para lógica compleja con múltiples combinaciones:

Antes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
char *obtener_categoria(int edad, bool estudiante, bool empleado)
{
    if (edad < 18 && estudiante)
    {
        return "ESTUDIANTE_MENOR";
    }
    else if (edad < 18 && !estudiante)
    {
        return "MENOR";
    }
    else if (edad >= 18 && edad < 65 && estudiante)
    {
        return "ESTUDIANTE_ADULTO";
    }
    else if (edad >= 18 && edad < 65 && empleado)
    {
        return "EMPLEADO";
    }
    else if (edad >= 18 && edad < 65)
    {
        return "ADULTO";
    }
    else if (edad >= 65 && empleado)
    {
        return "JUBILADO_ACTIVO";
    }
    else
    {
        return "JUBILADO";
    }
}

Después (con tabla):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
typedef struct
{
    bool (*condicion)(int edad, bool estudiante, bool empleado);
    const char *categoria;
} regla_categoria_t;
bool es_estudiante_menor(int edad, bool estudiante, bool empleado)
{
    return edad < 18 && estudiante;
}
bool es_menor(int edad, bool estudiante, bool empleado)
{
    return edad < 18 && !estudiante;
}
bool es_estudiante_adulto(int edad, bool estudiante, bool empleado)
{
    return edad >= 18 && edad < 65 && estudiante;
}
bool es_empleado(int edad, bool estudiante, bool empleado)
{
    return edad >= 18 && edad < 65 && empleado;
}
bool es_adulto(int edad, bool estudiante, bool empleado)
{
    return edad >= 18 && edad < 65 && !estudiante && !empleado;
}
bool es_jubilado_activo(int edad, bool estudiante, bool empleado)
{
    return edad >= 65 && empleado;
}
bool es_jubilado(int edad, bool estudiante, bool empleado)
{
    return edad >= 65 && !empleado;
}
const regla_categoria_t REGLAS_CATEGORIA[] = {
    {es_estudiante_menor, "ESTUDIANTE_MENOR"},
    {es_menor, "MENOR"},
    {es_estudiante_adulto, "ESTUDIANTE_ADULTO"},
    {es_empleado, "EMPLEADO"},
    {es_adulto, "ADULTO"},
    {es_jubilado_activo, "JUBILADO_ACTIVO"},
    {es_jubilado, "JUBILADO"}};
const int NUM_REGLAS = sizeof(REGLAS_CATEGORIA) / sizeof(REGLAS_CATEGORIA[0]);
const char *obtener_categoria(int edad, bool estudiante, bool empleado)
{
    for (int i = 0; i < NUM_REGLAS; i++)
    {
        if (REGLAS_CATEGORIA[i].condicion(edad, estudiante, empleado))
        {
            return REGLAS_CATEGORIA[i].categoria;
        }
    }
    return "DESCONOCIDO";
}

7. Uso de switch en lugar de if-else en cadena

Para comparaciones de igualdad con un valor:

Antes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void procesar_comando(char comando)
{
    if (comando == 'A')
    {
        avanzar();
    }
    else if (comando == 'R')
    {
        retroceder();
    }
    else if (comando == 'I')
    {
        girar_izquierda();
    }
    else if (comando == 'D')
    {
        girar_derecha();
    }
    else if (comando == 'P')
    {
        parar();
    }
    else
    {
        printf("Comando inválido\n");
    }
}

Después:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void procesar_comando(char comando)
{
    switch (comando)
    {
    case 'A':
        avanzar();
        break;
    case 'R':
        retroceder();
        break;
    case 'I':
        girar_izquierda();
        break;
    case 'D':
        girar_derecha();
        break;
    case 'P':
        parar();
        break;
    default:
        printf("Comando inválido\n");
        break;
    }
}

Casos Prácticos Completos

Caso 1: Validación de Formulario

Código Original:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
bool validar_formulario(const char *nombre, const char *email,
                        const char *telefono, int edad)
{
    if (nombre != NULL)
    {
        if (strlen(nombre) >= 3)
        {
            if (email != NULL)
            {
                if (strchr(email, '@') != NULL)
                {
                    if (strlen(email) >= 5)
                    {
                        if (telefono != NULL)
                        {
                            if (strlen(telefono) >= 8)
                            {
                                if (edad >= 18)
                                {
                                    if (edad <= 120)
                                    {
                                        return true;
                                    }
                                    else
                                    {
                                        printf("Edad muy alta\n");
                                    }
                                }
                                else
                                {
                                    printf("Debe ser mayor de edad\n");
                                }
                            }
                            else
                            {
                                printf("Teléfono muy corto\n");
                            }
                        }
                        else
                        {
                            printf("Teléfono nulo\n");
                        }
                    }
                    else
                    {
                        printf("Email muy corto\n");
                    }
                }
                else
                {
                    printf("Email sin @\n");
                }
            }
            else
            {
                printf("Email nulo\n");
            }
        }
        else
        {
            printf("Nombre muy corto\n");
        }
    }
    else
    {
        printf("Nombre nulo\n");
    }
    return false;
}

Código Refactorizado:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
bool validar_nombre(const char *nombre)
{
    if (nombre == NULL)
    {
        printf("Nombre nulo\n");
        return false;
    }
    if (strlen(nombre) < 3)
    {
        printf("Nombre muy corto\n");
        return false;
    }
    return true;
}
bool validar_email(const char *email)
{
    if (email == NULL)
    {
        printf("Email nulo\n");
        return false;
    }
    if (strlen(email) < 5)
    {
        printf("Email muy corto\n");
        return false;
    }
    if (strchr(email, '@') == NULL)
    {
        printf("Email sin @\n");
        return false;
    }
    return true;
}
bool validar_telefono(const char *telefono)
{
    if (telefono == NULL)
    {
        printf("Teléfono nulo\n");
        return false;
    }
    if (strlen(telefono) < 8)
    {
        printf("Teléfono muy corto\n");
        return false;
    }
    return true;
}
bool validar_edad(int edad)
{
    if (edad < 18)
    {
        printf("Debe ser mayor de edad\n");
        return false;
    }
    if (edad > 120)
    {
        printf("Edad muy alta\n");
        return false;
    }
    return true;
}
bool validar_formulario(const char *nombre, const char *email,
                        const char *telefono, int edad)
{
    return validar_nombre(nombre) && validar_email(email) &&
           validar_telefono(telefono) && validar_edad(edad);
}

Caso 2: Cálculo de Descuento Complejo

Código Original:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
double calcular_descuento(double monto, int cantidad, bool es_miembro,
                          bool es_primera_compra, const char *codigo_promo)
{
    double descuento = 0;
    if (cantidad > 10 && monto > 1000)
    {
        if (es_miembro)
        {
            if (strcmp(codigo_promo, "VERANO") == 0)
            {
                descuento = 0.30;
            }
            else if (strcmp(codigo_promo, "FLASH") == 0)
            {
                if (cantidad > 20)
                {
                    descuento = 0.35;
                }
                else
                {
                    descuento = 0.25;
                }
            }
            else
            {
                descuento = 0.20;
            }
        }
        else
        {
            if (es_primera_compra)
            {
                descuento = 0.15;
            }
            else
            {
                descuento = 0.10;
            }
        }
    }
    else if (cantidad > 5 || monto > 500)
    {
        if (es_miembro)
        {
            descuento = 0.15;
        }
        else
        {
            descuento = 0.05;
        }
    }
    else
    {
        if (es_primera_compra)
        {
            descuento = 0.10;
        }
    }
    return descuento;
}

Código Refactorizado:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
bool es_compra_grande(double monto, int cantidad)
{
    return cantidad > 10 && monto > 1000;
}
bool es_compra_mediana(double monto, int cantidad)
{
    return cantidad > 5 || monto > 500;
}
double descuento_miembro_compra_grande(int cantidad, const char *codigo_promo)
{
    if (strcmp(codigo_promo, "VERANO") == 0)
    {
        return 0.30;
    }
    if (strcmp(codigo_promo, "FLASH") == 0)
    {
        return cantidad > 20 ? 0.35 : 0.25;
    }
    return 0.20;
}
double descuento_no_miembro_compra_grande(bool es_primera_compra)
{
    return es_primera_compra ? 0.15 : 0.10;
}
double calcular_descuento(double monto, int cantidad, bool es_miembro,
                          bool es_primera_compra, const char *codigo_promo)
{
    if (es_compra_grande(monto, cantidad))
    {
        if (es_miembro)
        {
            return descuento_miembro_compra_grande(cantidad, codigo_promo);
        }
        else
        {
            return descuento_no_miembro_compra_grande(es_primera_compra);
        }
    }
    if (es_compra_mediana(monto, cantidad))
    {
        return es_miembro ? 0.15 : 0.05;
    }
    return es_primera_compra ? 0.10 : 0.0;
}

Leyes de De Morgan

Útiles para simplificar expresiones booleanas complejas:

Leyes:

Ejemplo:

1
2
3
4
5
6
7
8
9
10
// Antes
if (!(activo && validado))
{
    return;
}
// Después (más claro en contexto de guardia)
if (!activo || !validado)
{
    return;
}
1
2
3
4
5
6
7
8
9
10
// Antes
if (!(edad < 18 || edad > 65))
{
    aplicar_tarifa_regular();
}
// Después (más claro)
if (edad >= 18 && edad <= 65)
{
    aplicar_tarifa_regular();
}

Negar una disyunción y reescribirla como conjunción afirmativa es una aplicación directa de 0x1017h: Escribí condiciones afirmativas y directas: la condición queda en forma positiva y directa. Además, el rango se expresa como edad >= 18 && edad <= 65 con && explícito, nunca como 18 <= edad <= 65, tal como prohíbe 0x1010h: Prohibición de comparaciones encadenadas no idiomáticas en C (a < b < c).

Ejemplo Integrador: Varias Reglas en una Sola Función

Hasta acá vimos las técnicas por separado. En el código real suelen aparecer varias violaciones a la vez, y la refactorización consiste en aplicarlas todas sobre la misma función. El siguiente ejemplo reúne la mayoría de las reglas de la tabla inicial.

Antes (❌):

int procesar_envio(pedido_t *pedido)
{
    if (pedido != NULL)
    {
        if (pedido->items > 0)
        {
            if (pedido->peso < 50)
            {
                if (pedido->cliente != NULL)
                {
                    int zona;
                    if ((zona = calcular_zona(pedido)) == ZONA_NACIONAL)
                    {
                        if (pedido->prioridad && 0 < pedido->distancia < 1000 || pedido->cliente->vip)
                        {
                            return ENVIO_EXPRESS;
                        }
                        else
                        {
                            return ENVIO_ESTANDAR;
                        }
                    }
                    else
                    {
                        return ENVIO_INTERNACIONAL;
                    }
                }
                else
                {
                    return SIN_CLIENTE;
                }
            }
            else
            {
                return PESO_EXCEDIDO;
            }
        }
        else
        {
            return SIN_ITEMS;
        }
    }
    else
    {
        return PEDIDO_NULO;
    }
}

Este código viola, al mismo tiempo:

Después (✅):

bool es_envio_nacional(int zona)
{
    return zona == ZONA_NACIONAL;
}

bool es_envio_corto(const pedido_t *pedido)
{
    return pedido->distancia > 0 && pedido->distancia < 1000;
}

int procesar_envio(const pedido_t *pedido)
{
    // Guardas: el camino de error sale primero (0x2001h, 0x1015h)
    if (pedido == NULL)
    {
        return PEDIDO_NULO;
    }
    if (pedido->items == 0)
    {
        return SIN_ITEMS;
    }
    if (pedido->peso >= 50)
    {
        return PESO_EXCEDIDO;
    }
    if (pedido->cliente == NULL)
    {
        return SIN_CLIENTE;
    }

    // La asignación va en su propia sentencia (0x1012h, 0x1009h)
    int zona = calcular_zona(pedido);

    // Variable a la izquierda, condición afirmativa (0x100Bh, 0x1017h)
    if (!es_envio_nacional(zona))
    {
        return ENVIO_INTERNACIONAL;
    }

    // Rango explícito con && y comparación de bool directa (0x1010h, 0x1005h)
    if (pedido->prioridad == true &&
        (es_envio_corto(pedido) || pedido->cliente->vip))
    {
        return ENVIO_EXPRESS;
    }
    return ENVIO_ESTANDAR;
}

Después de la refactorización no queda ningún else tras un return, no hay más de dos niveles de anidación, y cada subexpresión tiene nombre o comparación explícita. La lógica que antes dependía de la precedencia y del orden de evaluación ahora se lee tal cual se ejecuta; lo que cambió es cuánto esfuerzo cuesta leerla y auditarla.

Combinación con el Patrón de Único Retorno

Cuando se aplica el patrón de único retorno (ver Combinación con el Patrón de Único Retorno), las técnicas de simplificación de condicionales deben adaptarse para mantener un solo punto de salida.

Guardia de Cláusulas con Único Retorno

Guard Clauses tradicionales (múltiples returns):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int procesar_pago(usuario_t *usuario, double monto)
{
    if (usuario == NULL)
    {
        return ERROR_USUARIO_NULL;
    }
    if (!usuario->activo)
    {
        return ERROR_USUARIO_INACTIVO;
    }
    if (usuario->saldo < monto)
    {
        return ERROR_SALDO_INSUFICIENTE;
    }
    // Procesar pago
    usuario->saldo -= monto;
    return EXITO;
}

Adaptado a único retorno:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int procesar_pago(usuario_t *usuario, double monto)
{
    int resultado = ERROR_DESCONOCIDO;
    if (usuario == NULL)
    {
        resultado = ERROR_USUARIO_NULL;
    }
    else if (!usuario->activo)
    {
        resultado = ERROR_USUARIO_INACTIVO;
    }
    else if (usuario->saldo < monto)
    {
        resultado = ERROR_SALDO_INSUFICIENTE;
    }
    else
    {
        // Procesar pago
        usuario->saldo -= monto;
        resultado = EXITO;
    }
    return resultado;
}

Variables de Estado para Control de Flujo

Cuando se combina único retorno con condicionales complejos, usar variables de estado explícitas mejora la claridad.

Antes (múltiples returns):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool validar_transaccion(transaccion_t *trans)
{
    if (trans->monto <= 0)
    {
        return false;
    }
    if (trans->origen == NULL || trans->destino == NULL)
    {
        return false;
    }
    if (trans->origen->saldo < trans->monto)
    {
        return false;
    }
    if (trans->origen->bloqueada || trans->destino->bloqueada)
    {
        return false;
    }
    return true;
}

Después (único retorno con variable de estado):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool validar_transaccion(const transaccion_t *trans)
{
    bool es_valida = true;
    if (trans->monto <= 0)
    {
        es_valida = false;
    }
    else if (trans->origen == NULL || trans->destino == NULL)
    {
        es_valida = false;
    }
    else if (trans->origen->saldo < trans->monto)
    {
        es_valida = false;
    }
    else if (trans->origen->bloqueada || trans->destino->bloqueada)
    {
        es_valida = false;
    }
    return es_valida;
}

Combinando Predicados y Único Retorno

Con múltiples returns:

1
2
3
4
5
6
7
8
9
10
bool puede_realizar_compra(const usuario_t *u, double monto)
{
    if (!es_usuario_valido(u))
        return false;
    if (!tiene_saldo_suficiente(u, monto))
        return false;
    if (!esta_dentro_limite_diario(u, monto))
        return false;
    return true;
}

Con único retorno:

1
2
3
4
5
6
7
8
9
10
bool puede_realizar_compra(const usuario_t *u, double monto)
{
    bool puede = false;
    if (es_usuario_valido(u) && tiene_saldo_suficiente(u, monto) &&
        esta_dentro_limite_diario(u, monto))
    {
        puede = true;
    }
    return puede;
}

O de manera más concisa:

bool puede_realizar_compra(const usuario_t *u, double monto)
{
    return es_usuario_valido(u) && tiene_saldo_suficiente(u, monto) &&
           esta_dentro_limite_diario(u, monto);
}

Caso Práctico: Autenticación con Único Retorno

Antes (múltiples returns):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int autenticar(const char *usuario, const char *password)
{
    if (usuario == NULL || password == NULL)
    {
        return AUTH_ERROR_PARAMETROS;
    }
    usuario_t *u = buscar_usuario(usuario);
    if (u == NULL)
    {
        return AUTH_ERROR_USUARIO_NO_EXISTE;
    }
    if (u->intentos_fallidos >= MAX_INTENTOS)
    {
        return AUTH_ERROR_BLOQUEADO;
    }
    if (!verificar_password(u, password))
    {
        u->intentos_fallidos++;
        return AUTH_ERROR_PASSWORD_INCORRECTO;
    }
    u->intentos_fallidos = 0;
    u->ultimo_acceso = time(NULL);
    return AUTH_EXITO;
}

Después (único retorno con flujo claro):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int autenticar(const char *usuario, const char *password)
{
    int resultado = AUTH_ERROR_DESCONOCIDO;
    usuario_t *u = NULL;
    // Validación de parámetros
    if (usuario == NULL || password == NULL)
    {
        resultado = AUTH_ERROR_PARAMETROS;
    }
    else
    {
        u = buscar_usuario(usuario);
        if (u == NULL)
        {
            resultado = AUTH_ERROR_USUARIO_NO_EXISTE;
        }
        else if (u->intentos_fallidos >= MAX_INTENTOS)
        {
            resultado = AUTH_ERROR_BLOQUEADO;
        }
        else if (!verificar_password(u, password))
        {
            u->intentos_fallidos++;
            resultado = AUTH_ERROR_PASSWORD_INCORRECTO;
        }
        else
        {
            // Autenticación exitosa
            u->intentos_fallidos = 0;
            u->ultimo_acceso = time(NULL);
            resultado = AUTH_EXITO;
        }
    }
    return resultado;
}

Manejo de Recursos con Único Retorno

El patrón de único retorno es especialmente útil cuando se manejan recursos que deben liberarse.

Antes (múltiples returns, riesgo de fugas):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
char *leer_archivo(const char *ruta)
{
    FILE *f = fopen(ruta, "r");
    if (f == NULL)
    {
        return NULL;
    }
    fseek(f, 0, SEEK_END);
    long tam = ftell(f);
    if (tam < 0)
    {
        fclose(f); // Fácil olvidar esto
        return NULL;
    }
    char *buffer = malloc(tam + 1);
    if (buffer == NULL)
    {
        fclose(f); // Y esto
        return NULL;
    }
    fseek(f, 0, SEEK_SET);
    size_t leidos = fread(buffer, 1, tam, f);
    if (leidos != tam)
    {
        free(buffer); // Y esto
        fclose(f);    // Y esto
        return NULL;
    }
    buffer[tam] = '\0';
    fclose(f);
    return buffer;
}

Después (único retorno, liberación garantizada):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
char *leer_archivo(const char *ruta)
{
    char *resultado = NULL;
    FILE *f = NULL;
    char *buffer = NULL;
    f = fopen(ruta, "r");
    if (f != NULL)
    {
        fseek(f, 0, SEEK_END);
        long tam = ftell(f);
        if (tam >= 0)
        {
            buffer = malloc(tam + 1);
            if (buffer != NULL)
            {
                fseek(f, 0, SEEK_SET);
                size_t leidos = fread(buffer, 1, tam, f);
                if (leidos == tam)
                {
                    buffer[tam] = '\0';
                    resultado = buffer;
                    buffer = NULL; // No liberar si exitoso
                }
            }
        }
    }
    // Limpieza centralizada
    if (buffer != NULL)
    {
        free(buffer);
    }
    if (f != NULL)
    {
        fclose(f);
    }
    return resultado;
}

Variables de Control para Lazos

El patrón de único retorno se combina bien con variables de control en lazos.

Antes (con break/return):

1
2
3
4
5
6
7
8
9
10
11
int buscar_elemento(const int *arr, int n, int valor)
{
    for (int i = 0; i < n; i++)
    {
        if (arr[i] == valor)
        {
            return i; // Retorno temprano
        }
    }
    return -1;
}

Después (único retorno):

1
2
3
4
5
6
7
8
9
10
11
12
int buscar_elemento(const int *arr, int n, int valor)
{
    int indice = -1;
    for (int i = 0; i < n && indice == -1; i++)
    {
        if (arr[i] == valor)
        {
            indice = i;
        }
    }
    return indice;
}

Balance entre Claridad y Único Retorno

Funciones simples - múltiples returns aceptables:

1
2
3
4
5
6
7
8
bool es_par(int n)
{
    return n % 2 == 0;
}
int maximo(int a, int b)
{
    return (a > b) ? a : b;
}

Funciones complejas - único retorno preferible:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int procesar_pedido_complejo(pedido_t *pedido, contexto_t *ctx)
{
    int resultado = ERROR_DESCONOCIDO;
    recurso_t *recurso = NULL;
    // Múltiples validaciones y manejo de recursos
    if (validar_pedido(pedido))
    {
        recurso = adquirir_recurso(ctx);
        if (recurso != NULL)
        {
            if (procesar_con_recurso(pedido, recurso))
            {
                resultado = EXITO;
            }
            else
            {
                resultado = ERROR_PROCESAMIENTO;
            }
        }
        else
        {
            resultado = ERROR_RECURSO;
        }
    }
    else
    {
        resultado = ERROR_VALIDACION;
    }
    // Limpieza garantizada
    if (recurso != NULL)
    {
        liberar_recurso(recurso);
    }
    return resultado;
}

Diagnóstico y refactorización

Para auditar código existente, conviene recorrer los síntomas de izquierda a derecha y aplicar la técnica correspondiente. La siguiente tabla funciona como guía rápida de diagnóstico.

ReglaSíntoma en el códigoTécnica de esta guía
0x1004h: Las condiciones complejas deben simplificarse o comentarseCondición larga con varios && y `
0x1013h: Detector de expresiones booleanas complejas sin paréntesis aclaratoriosMezcla de && y `
0x1005h: Reemplazá las condiciones ambiguas basadas en la ‘veracidad’ (truthiness) del tipo de datoif (puntero), if (entero) o if (es_valido() == true)Comparación explícita contra NULL, 0 o true/false
0x100Bh: No utilices comparaciones en estilo Yoda (‘CONST == variable’)if (NULL == puntero), while (0 == i)Reordenar a variable == CONSTANTE
0x1012h: Prohibición de expresiones de asignación dentro de estructuras de controlif ((x = f()) != 0)Extraer la asignación a una sentencia propia
0x1009h: Prohibición de asignaciones simples dentro de condiciones lógicaswhile (c = getchar()) con = simpleSeparar asignación y comparar con == en la condición
0x1010h: Prohibición de comparaciones encadenadas no idiomáticas en C (a < b < c)0 <= x <= 10, a < b < cReescribir como a < b && b < c
0x1017h: Escribí condiciones afirmativas y directasif (!(activo && validado))Forma afirmativa o variable con nombre positivo
0x1015h: Limitá la profundidad de anidación a tres nivelesPirámide de if con más de tres nivelesGuard clauses, inversión de condición y extracción de función
0x2001h: Las funciones deben usar cláusulas de guarda y retornos anticipados para reducir la anidación profundaValidaciones al final del camino felizCláusulas de guarda al inicio con retornos anticipados
0x100Fh: Prohibición de cláusula else redundante tras sentencia de retorno anticipadoelse después de una rama que retornaDesanidar la rama alterna al nivel del if
0x2013h: Detector de bloques else superfluos tras sentencias terminaleselse tras return, break, continue o gotoEliminar el else y continuar el flujo

Resumen

Técnicas para simplificar condicionales:

  1. Guard Clauses: Validaciones tempranas para reducir anidamiento

  2. Variables Booleanas: Dar nombres descriptivos a condiciones complejas

  3. Funciones de Predicado: Encapsular lógica condicional reutilizable

  4. Simplificación Booleana: Retornar expresiones directamente

  5. Polimorfismo: Reemplazar condicionales con despacho dinámico

  6. Tablas de Decisión: Para lógica compleja con muchas combinaciones

  7. Switch: En lugar de cadenas if-else para valores discretos

  8. Único Retorno: Combinar con condicionales para mejor manejo de recursos

Principios clave:

La claridad en los condicionales es esencial para código mantenible y libre de bugs.

Checklist de verificación

Antes de dar por cerrada una refactorización de condicionales, verificá: