Fundamentos de la complejidad ciclomática

Los conceptos básicos de la complejidad ciclomática y por qué todo programador debería conocerlos

Cada función que escribes tiene una puntuación de complejidad. Si la función no contiene puntos de decisión, no ifEn else, sin bucle, no switch, su complejidad ciclomática es 1: exactamente un camino a través del código. Agregue un if declaración y se convierte en 2. Agregue otra y se convierte en 3. Para cuando una función ha acumulado una docena de ramas condicionales en varios niveles anidados, su complejidad ciclomática puede ser de 15 o 20, y probarla completamente requiere un número correspondiente de casos de prueba, cada uno cubriendo una ruta de ejecución distinta.

La complejidad ciclomática (CC) fue introducida por Thomas J. McCabe en 1976 como una medida cuantitativa de la complejidad lógica del flujo de control de un programa. Sigue siendo una de las métricas de calidad de código más útiles en la práctica, ya que sus implicaciones son concretas y aplicables: una puntuación de complejidad indica el número mínimo de casos de prueba necesarios para una cobertura completa de la ruta, predice la dificultad de comprensión y modificación del código e identifica las funciones con mayor probabilidad de contener defectos no detectados. Esta guía abarca la fórmula, los umbrales, ejemplos específicos para cada lenguaje, técnicas de refactorización que realmente reducen la complejidad y cómo medirla y monitorizarla automáticamente.

SMART TS XL

Le ayuda a dominar la complejidad ciclomática, optimizar el rendimiento y prevenir errores ocultos

DESCUBRE MÁS…

¿Qué es la complejidad ciclomática?

La complejidad ciclomática mide el número de rutas linealmente independientes a través del código fuente de un programa. Thomas J. McCabe la derivó de la teoría de grafos: todo programa puede representarse como un grafo de flujo de control donde los nodos son instrucciones y las aristas son los posibles flujos entre ellas. La fórmula es:

CC = E - N + 2P

Lugar:

  • E = número de aristas en el grafo de flujo de control
  • N = número de nodos
  • P = número de componentes conectadas (normalmente 1 para una sola función)

Para cálculos prácticos, existe un equivalente más sencillo: CC = número de puntos de decisión + 1. Cada if, else if, while, for, case, catch, &&, y || agrega un punto de decisión. El CC inicial de cualquier función es 1.

Java

// CC = 1: no decision points
public String greet(String name) {
    return "Hello, " + name;
}

// CC = 3: two decision points (two if statements)
public String classify(int score) {
    if (score >= 90) return "Excellent";
    if (score >= 70) return "Satisfactory";
    return "Needs improvement";
}

// CC = 5: four decision points (three conditions + one loop)
public double calculateTotal(List<Item> items, boolean isMember, boolean isHoliday) {
    double total = 0;
    for (Item item : items) {          // +1
        total += item.getPrice();
    }
    if (isMember) total *= 0.9;        // +1
    if (isHoliday) total *= 0.95;      // +1
    if (total > 100) total -= 5;       // +1
    return total;
}

Umbrales: ¿Qué puntuación es aceptable?

La guía original de McCabe, que sigue siendo la más citada, define cuatro niveles de riesgo:

Puntuación CCNivel de riesgoInterpretación
1 – 10 BajoSencillo, bien estructurado, fácil de probar.
11 – 20 ModeradoMás complejo; se requiere un mayor esfuerzo de prueba.
21 – 50 AltoComplejo y difícil de probar; se recomienda la refactorización.
> 50Muy AltaImposible de comprobar en la práctica; grave riesgo para la calidad.

El umbral de 10 es el límite más comúnmente aplicado en los controles de calidad de CI/CD. El umbral de complejidad cognitiva predeterminado de SonarQube es 15 (una métrica relacionada pero distinta). Las directrices del NIST para sistemas críticos para la seguridad recomiendan un máximo de 10 por módulo.

Un matiz importante: CC mide la complejidad estructural, no la semántica. Una función con CC = 8 que implementa un cálculo financiero complejo puede ser más difícil de entender que una función con CC = 15 que consiste en comprobaciones defensivas sencillas. Utilice CC como una señal para investigar, no como un veredicto final.

Complejidad ciclomática en diferentes lenguajes

Python

Estructuras de decisión de Python que contribuyen a la CC: if, elif, else (no se cuenta, no tiene condición), for, while, try/except (cada except recuentos), with (no cuenta) y operadores booleanos and/or en condiciones.

pitón

# CC = 1
def format_name(first: str, last: str) -> str:
    return f"{first} {last}"

# CC = 4: three decision points
def calculate_discount(price: float, is_member: bool, is_holiday: bool) -> float:
    discount = 0.0
    if is_member:        # +1
        discount += 0.10
    if is_holiday:       # +1
        discount += 0.05
    if price > 100:      # +1
        discount += 0.02
    return price * (1 - discount)

# CC = 6: five decision points (list comprehension counts as a loop)
def process_orders(orders: list[dict]) -> list[dict]:
    return [
        {**order, "total": order["qty"] * order["price"]}  # +1 (comprehension)
        for order in orders
        if order["qty"] > 0                                 # +1 (filter condition)
        if order["price"] > 0                              # +1 (second filter)
    ]

Herramientas para la medición de CC en Python: radon (radon cc src/ -s), flake8-cognitive-complexity, pylint Con el complemento de complejidad, análisis de SonarQube Python.

Java

Java

// CC = 7: complex authentication with multiple conditions
public AuthResult authenticate(String userId, String password, boolean isMfa) {
    if (userId == null || password == null) return AuthResult.INVALID;  // +2 (||)
    User user = userRepository.findById(userId);
    if (user == null) return AuthResult.NOT_FOUND;                      // +1
    if (!user.checkPassword(password)) return AuthResult.WRONG_PASSWORD;// +1
    if (isMfa && !user.hasMfaEnabled()) return AuthResult.MFA_REQUIRED; // +2 (&&)
    return AuthResult.SUCCESS;
}
// CC = 1 + 2 + 1 + 1 + 2 = 7

Reduciéndolo:

Java

// After refactoring: CC = 3 (main method) + small helpers with CC = 2 each
public AuthResult authenticate(String userId, String password, boolean isMfa) {
    if (hasInvalidInputs(userId, password)) return AuthResult.INVALID;
    User user = findVerifiedUser(userId, password);
    if (user == null) return AuthResult.WRONG_PASSWORD;
    if (requiresMfa(user, isMfa)) return AuthResult.MFA_REQUIRED;
    return AuthResult.SUCCESS;
}

private boolean hasInvalidInputs(String userId, String password) {
    return userId == null || password == null;  // CC = 2
}

private boolean requiresMfa(User user, boolean isMfa) {
    return isMfa && !user.hasMfaEnabled();      // CC = 2
}

Herramientas para Java CC: Checkstyle, PMD, SonarQube, integrado en IntelliJ IDEA, SMART TS XL.

C# y TypeScript

C# y TypeScript siguen las mismas reglas que Java. La principal diferencia radica en que las cláusulas de expresiones LINQ en C# y las cadenas ternarias en TypeScript añaden puntos de decisión.

CSharp

// CC = 5: switch with four cases
public decimal GetShippingCost(string zone) => zone switch {
    "domestic"      => 5.99m,    // +1
    "eu"            => 15.99m,   // +1
    "international" => 29.99m,   // +1
    "express"       => 49.99m,   // +1
    _               => throw new ArgumentException($"Unknown zone: {zone}")
};

COBOL

Estructuras de decisión de COBOL que contribuyen a CC: IF/ELSE, EVALUATE WHEN (cada cláusula WHEN), PERFORM UNTIL, PERFORM VARYING ... WITH TEST BEFORE/AFTER, AT END, ON EXCEPTION, NOT ON EXCEPTION, ON SIZE ERROR.

cobol

       CALCULATE-DISCOUNT.
           IF WS-CUSTOMER-TYPE = 'GOLD'               *> +1
               IF WS-PURCHASE-AMT > 1000              *> +1
                   COMPUTE WS-DISCOUNT = 0.20
               ELSE                                    *> (no increment)
                   COMPUTE WS-DISCOUNT = 0.15
           ELSE IF WS-CUSTOMER-TYPE = 'SILVER'        *> +1
               COMPUTE WS-DISCOUNT = 0.10
           ELSE                                        *> (no increment)
               COMPUTE WS-DISCOUNT = 0.05
           END-IF
           EVALUATE TRUE
               WHEN WS-REGION = 'NORTH' PERFORM APPLY-REGIONAL-RATE  *> +1
               WHEN WS-REGION = 'SOUTH' PERFORM APPLY-SOUTHERN-RATE  *> +1
           END-EVALUATE.
           *> Total CC = 1 + 5 = 6

La sintaxis verbosa de COBOL implica que los párrafos suelen ser más largos que las funciones equivalentes en lenguajes modernos. Los programas COBOL con más de 50 caracteres CC por párrafo son comunes en bases de código heredadas y representan los objetivos de máxima prioridad tanto para la refactorización como para la planificación de la modernización.

Cómo calcular la complejidad ciclomática: tres métodos

Método 1: Contar puntos de decisión + 1 El método manual más rápido. Cuenta cada if, else if, while, for, case, catch, &&, || en la función. Sume 1 por la función misma.

Método 2: Grafo de flujo de control Dibuja la función como un grafo: un nodo por instrucción o bloque, aristas para cada flujo de control. Aplica CC = E - N + 2.

Método 3: Herramienta automatizada. El único método práctico para cualquier función que vaya más allá de lo trivial. La mayoría de las herramientas de análisis estático calculan el CC automáticamente y lo integran en los pipelines de CI/CD.

Técnicas de refactorización que realmente reducen la complejidad

Cláusulas de salvaguardia (devoluciones anticipadas)

Las cláusulas de protección finalizan la función anticipadamente cuando fallan las precondiciones, eliminando así las bifurcaciones else y reduciendo la profundidad de anidamiento.

pitón

# Before: deeply nested, CC = 5
def process_order(order):
    if order is not None:
        if order.is_valid():
            if order.has_stock():
                if order.payment_cleared():
                    return fulfill_order(order)
                else:
                    return "Payment failed"
            else:
                return "Out of stock"
        else:
            return "Invalid order"
    else:
        return "No order"

# After: flat, CC = 5 (same complexity, dramatically better readability)
def process_order(order):
    if order is None: return "No order"
    if not order.is_valid(): return "Invalid order"
    if not order.has_stock(): return "Out of stock"
    if not order.payment_cleared(): return "Payment failed"
    return fulfill_order(order)

La complejidad computacional (CC) no disminuye; los puntos de decisión siguen siendo los mismos, pero el código se vuelve mucho más fácil de leer y probar. Una verdadera reducción de la CC requiere eliminar puntos de decisión, no solo reorganizarlos.

Métodos de extracción

Trasladar grupos lógicos de decisiones a métodos con nombre reduce la complejidad computacional de la función que realiza la llamada, al tiempo que distribuye la complejidad entre unidades más pequeñas y comprobables.

Java

// Before: one method doing everything, CC = 9
public double calculateInvoiceTotal(Invoice invoice, Customer customer) {
    double subtotal = 0;
    for (LineItem item : invoice.getItems()) {
        subtotal += item.getQuantity() * item.getUnitPrice();
        if (item.isTaxable()) subtotal += item.getPrice() * 0.1;
    }
    if (customer.isMember()) subtotal *= 0.9;
    if (customer.hasVoucher()) subtotal -= customer.getVoucherValue();
    if (subtotal < 0) subtotal = 0;
    return subtotal;
}

// After: main method CC = 4, helpers have CC = 2-3 each
public double calculateInvoiceTotal(Invoice invoice, Customer customer) {
    double subtotal = computeLineItemTotal(invoice.getItems());
    subtotal = applyCustomerDiscounts(subtotal, customer);
    return Math.max(0, subtotal);
}

Sustitución de condicionales por polimorfismo

Cuando una función se ramifica en función del tipo o del estado, el polimorfismo elimina por completo la ramificación.

Java

// Before: switch on payment type, CC grows with each new type
public void processPayment(String type, double amount) {
    switch (type) {
        case "CREDIT":  processCreditCard(amount); break;
        case "PAYPAL":  processPayPal(amount);     break;
        case "CRYPTO":  processCrypto(amount);     break;
        default: throw new IllegalArgumentException("Unknown type: " + type);
    }
}

// After: new payment types require no changes to this method, CC = 1
public interface PaymentProcessor {
    void process(double amount);
}

public void processPayment(PaymentProcessor processor, double amount) {
    processor.process(amount);  // no branching
}

Descomposición de condicionales complejos

Extrae expresiones booleanas complejas y conviértelas en métodos con nombre que revelen su intención.

pitón

# Before: dense boolean logic, hard to understand, easy to mis-test
if user.age >= 18 and user.country in ALLOWED_COUNTRIES and not user.is_banned and user.verified:
    grant_access()

# After: named predicate, self-documenting, unit-testable independently
def is_eligible_for_access(user: User) -> bool:
    return (
        user.age >= 18
        and user.country in ALLOWED_COUNTRIES
        and not user.is_banned
        and user.verified
    )

if is_eligible_for_access(user):
    grant_access()

Complejidad ciclomática en pipelines de CI/CD

La aplicación automatizada de las normas de control de versiones en los pipelines de CI/CD evita que la complejidad se acumule de forma invisible entre las revisiones de código.

yaml

# GitHub Actions: fail PR if any function exceeds CC threshold
name: Code Quality

on: [pull_request]

jobs:
  complexity-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install radon (Python CC tool)
        run: pip install radon

      - name: Check cyclomatic complexity
        run: |
          radon cc src/ --min C --show-complexity
          # Fails if any function has CC grade C (11-15) or worse
          radon cc src/ --min C --total-average | grep -q "Average complexity" \
            && echo "Complexity check passed" \
            || (echo "Functions with high complexity found" && exit 1)

Para Java con SonarQube:

yaml

# SonarQube quality gate blocks merge if CC exceeds threshold
- name: SonarCloud Scan
  uses: SonarSource/sonarcloud-github-action@master
  env:
    SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
  with:
    args: |
      -Dsonar.qualitygate.wait=true
      -Dsonar.java.complexity.Function.threshold=10

El control de calidad debería aplicarse al código nuevo de alta complejidad, no a toda la base de código heredada, que puede tener ya una alta complejidad condicional que se está abordando por separado.

Complejidad ciclomática en COBOL y bases de código heredadas

En sistemas empresariales donde los programas COBOL pueden tener párrafos con CC que superan los 50 o incluso los 100, el análisis de complejidad ciclomática cumple una función principal distinta a la que tiene en bases de código modernas. La pregunta no es "¿deberíamos refactorizar esta función?", sino "¿cuál es el riesgo de migración de este programa y en qué orden deberíamos modernizarlo?".

Un programa COBOL con CC = 80 en su párrafo principal tiene 80 rutas de ejecución independientes, cada una de las cuales requiere un caso de prueba para su validación. Si el programa carece de casos de prueba, como suele ocurrir con la mayoría de los programas COBOL heredados, la puntuación CC es el principal indicador de cuántos escenarios de validación deben crearse antes de que cualquier trabajo de conversión pueda considerarse seguro.

SMART TS XL, análisis de código estático Calcula la complejidad ciclomática para COBOL, JCL, RPG, PL/I y todos los lenguajes modernos simultáneamente, produciendo la distribución CC a nivel de portafolio que hace que las decisiones de secuenciación de modernización se basen en evidencia. Los programas con las puntuaciones CC más altas y la mayor cantidad de llamadas (alta entrada de fan-in) son los objetivos de migración de mayor riesgo, como se describe en el contexto de Establecer métricas de índice de mantenibilidad para aplicaciones COBOLCC es un componente de una imagen de calidad más amplia que también incluye el volumen de Halstead y las líneas de código.

La capacidad de análisis de impacto utiliza la clasificación de complejidad basada en CC para determinar qué se debe validar cuando se modifica un programa de alta complejidad: un valor de CC más alto implica más rutas de ejecución, lo que significa más escenarios de prueba que deben verificarse para confirmar la equivalencia de comportamiento antes y después de cualquier modificación.

Para los equipos que planifican programas de modernización de sistemas heredados , la distribución de CC en toda la cartera es el dato de entrada para la secuenciación de las oleadas de migración: los programas con bajo CC y pocos usuarios migran pronto; los programas con alto CC y muchos usuarios migran al final, después de que el equipo haya adquirido experiencia en los componentes más sencillos y la infraestructura de pruebas esté implementada para validar los más complejos.

La complejidad no es el enemigo, la complejidad invisible es

La complejidad ciclomática es una de las pocas métricas de calidad de código que se relaciona directamente con la capacidad de prueba. El número de casos de prueba necesarios para una cobertura completa de la ruta es, por definición, al menos igual a la puntuación CC. Esta relación la hace práctica y útil, a diferencia de muchas otras métricas de calidad.

La disciplina de gestionar el cambio climático requiere comprender que la complejidad se acumula de forma incremental. Cada if Una instrucción añadida a una función en crecimiento es razonable individualmente. El resultado de tres años de decisiones razonables individualmente puede ser una función con CC = 40 que nadie quiere modificar porque es demasiado compleja para analizarla con seguridad. Las herramientas y técnicas de esta guía (cláusulas de guarda, extracción de métodos, polimorfismo, descomposición condicional y controles de calidad de CI/CD) existen para evitar que esta acumulación se produzca de forma invisible y para abordarla sistemáticamente cuando ya se ha producido.