Como medir e reduzir complexidade ciclomática em código legado

1. Entendendo a complexidade ciclomática e seu impacto em sistemas legados

A complexidade ciclomática, formalmente definida por Thomas McCabe em 1976, é uma métrica que quantifica o número de caminhos linearmente independentes em um código-fonte. Ela é calculada como: M = E - N + 2P, onde E é o número de arestas, N o número de nós no grafo de fluxo de controle e P o número de componentes conectados.

Código legado tende a acumular alta complexidade ciclomática por diversas razões:
- Acúmulo de correções pontuais sem refatoração
- Múltiplos desenvolvedores adicionando condicionais sem visão holística
- Pressão por entregas rápidas que priorizam funcionalidade sobre estrutura
- Documentação deficiente que dificulta entender o fluxo completo

O impacto prático é direto: funções com complexidade ciclomática acima de 10 têm probabilidade 3x maior de conter bugs. Acima de 20, a testabilidade cai drasticamente e o custo de manutenção cresce exponencialmente.

2. Ferramentas e técnicas para medição precisa

Ferramentas de análise estática são essenciais para quantificar essa métrica de forma consistente:

SonarQube (configuração típica):

sonar.exclusions=**/generated/**
sonar.java.file.suffixes=.java
sonar.issue.ignore.multicriteria=e1
sonar.issue.ignore.multicriteria.e1.ruleKey=squid:MethodCyclomaticComplexity
sonar.issue.ignore.multicriteria.e1.resourceKey=**/test/**

ESLint (para JavaScript/TypeScript):

{
  "rules": {
    "complexity": ["error", { "max": 10 }]
  },
  "overrides": [
    {
      "files": ["*.test.js"],
      "rules": { "complexity": ["warn", { "max": 15 }] }
    }
  ]
}

PMD (para Java):

<rule ref="category/java/design.xml/CyclomaticComplexity">
  <properties>
    <property name="classReportLevel" value="80"/>
    <property name="methodReportLevel" value="10"/>
  </properties>
</rule>

Interpretação prática dos valores:
- 1-5: Baixa complexidade (código simples, fácil de testar)
- 6-10: Moderada (aceitável, mas requer atenção)
- 11-20: Alta (risco elevado de bugs, refatoração recomendada)
- 21+: Muito alta (crítico, refatoração obrigatória)

3. Estratégias de refatoração para redução imediata

Extração de métodos é a técnica mais direta. Considere este código legado:

function processOrder(order) {
  let total = 0;
  let discount = 0;
  let tax = 0;

  if (order.items.length > 0) {
    for (let item of order.items) {
      if (item.quantity > 0) {
        total += item.price * item.quantity;
        if (item.category === 'electronics') {
          discount += total * 0.05;
        }
      }
    }
  }

  if (total > 1000) {
    discount += total * 0.1;
  }

  if (order.customer.type === 'vip') {
    discount += total * 0.15;
  }

  tax = total * 0.08;
  return total - discount + tax;
}

Após extração e aplicação de cláusulas de guarda:

function processOrder(order) {
  if (!order?.items?.length) return 0;

  const total = calculateTotal(order.items);
  const discount = calculateDiscount(order, total);
  const tax = calculateTax(total);

  return total - discount + tax;
}

function calculateTotal(items) {
  return items
    .filter(item => item.quantity > 0)
    .reduce((sum, item) => sum + item.price * item.quantity, 0);
}

function calculateDiscount(order, total) {
  let discount = 0;
  if (total > 1000) discount += total * 0.1;
  if (order.customer.type === 'vip') discount += total * 0.15;
  return discount;
}

4. Lidando com estruturas de decisão complexas

Substituição de switch/case por polimorfismo:

Antes (complexidade ciclomática = 7):

function calculateShipping(type, weight) {
  let cost;
  switch (type) {
    case 'standard':
      cost = weight * 0.5;
      if (weight > 10) cost += 5;
      break;
    case 'express':
      cost = weight * 1.2;
      if (weight > 5) cost += 10;
      break;
    case 'overnight':
      cost = weight * 2.5;
      if (weight > 2) cost += 15;
      break;
    default:
      cost = weight * 0.3;
  }
  return cost;
}

Depois (complexidade ciclomática = 2 por estratégia):

const shippingStrategies = {
  standard: { baseRate: 0.5, threshold: 10, surcharge: 5 },
  express: { baseRate: 1.2, threshold: 5, surcharge: 10 },
  overnight: { baseRate: 2.5, threshold: 2, surcharge: 15 },
};

function calculateShipping(type, weight) {
  const strategy = shippingStrategies[type] || { baseRate: 0.3, threshold: Infinity, surcharge: 0 };
  let cost = weight * strategy.baseRate;
  if (weight > strategy.threshold) cost += strategy.surcharge;
  return cost;
}

5. Abordagens para loops e iterações com alta complexidade

Loops com múltiplas responsabilidades são fontes comuns de alta complexidade:

Antes (complexidade ciclomática = 8):

function processTransactions(transactions) {
  let validCount = 0;
  let totalAmount = 0;
  let fraudAlerts = [];
  let summary = {};

  for (let i = 0; i < transactions.length; i++) {
    const t = transactions[i];
    if (t.amount > 0 && t.status === 'completed') {
      validCount++;
      totalAmount += t.amount;

      if (t.amount > 10000) {
        fraudAlerts.push({ id: t.id, reason: 'high_value' });
      }

      const month = new Date(t.date).getMonth();
      summary[month] = (summary[month] || 0) + t.amount;
    }
  }
  return { validCount, totalAmount, fraudAlerts, summary };
}

Depois (complexidade ciclomática = 2 por função):

function processTransactions(transactions) {
  const validTransactions = transactions.filter(t => t.amount > 0 && t.status === 'completed');

  return {
    validCount: validTransactions.length,
    totalAmount: calculateTotalAmount(validTransactions),
    fraudAlerts: detectFraud(validTransactions),
    summary: buildMonthlySummary(validTransactions)
  };
}

function calculateTotalAmount(transactions) {
  return transactions.reduce((sum, t) => sum + t.amount, 0);
}

function detectFraud(transactions) {
  return transactions
    .filter(t => t.amount > 10000)
    .map(t => ({ id: t.id, reason: 'high_value' }));
}

function buildMonthlySummary(transactions) {
  return transactions.reduce((summary, t) => {
    const month = new Date(t.date).getMonth();
    summary[month] = (summary[month] || 0) + t.amount;
    return summary;
  }, {});
}

6. Estabelecendo limites e governança para evitar regressão

Para evitar que a complexidade retorne, estabeleça:

  1. Limites por camada:
  2. Controllers: max 5
  3. Services: max 10
  4. Repositories: max 8
  5. Utilitários: max 3

  6. Testes unitários como barreira de segurança:

describe('calculateTotal', () => {
  it('deve retornar 0 para lista vazia', () => {
    expect(calculateTotal([])).toBe(0);
  });

  it('deve calcular corretamente com itens válidos', () => {
    const items = [{ price: 10, quantity: 2 }, { price: 5, quantity: 3 }];
    expect(calculateTotal(items)).toBe(35);
  });

  it('deve ignorar itens com quantidade zero', () => {
    const items = [{ price: 10, quantity: 0 }, { price: 5, quantity: 3 }];
    expect(calculateTotal(items)).toBe(15);
  });
});
  1. Pipeline CI/CD: Configure gates que bloqueiem merges se a complexidade média por função exceder 8.

7. Caso prático: refatoração incremental de um módulo legado

Módulo original (complexidade ciclomática total: 45):

function validateAndProcessPayment(payment, customer, rules) {
  let isValid = true;
  let errors = [];
  let processedAmount = 0;

  if (payment.amount > 0) {
    if (payment.method === 'credit_card') {
      if (payment.cardNumber && payment.cardNumber.length === 16) {
        if (payment.expiryDate > new Date()) {
          if (payment.cvv && payment.cvv.length === 3) {
            processedAmount = payment.amount * 1.03;
          } else { errors.push('Invalid CVV'); isValid = false; }
        } else { errors.push('Card expired'); isValid = false; }
      } else { errors.push('Invalid card number'); isValid = false; }
    } else if (payment.method === 'paypal') {
      if (payment.email && payment.email.includes('@')) {
        processedAmount = payment.amount * 1.02;
      } else { errors.push('Invalid email'); isValid = false; }
    } else {
      errors.push('Unsupported payment method');
      isValid = false;
    }
  } else {
    errors.push('Invalid amount');
    isValid = false;
  }
  return { isValid, errors, processedAmount };
}

Após refatoração incremental (complexidade ciclomática total: 12):

function validateAndProcessPayment(payment, customer, rules) {
  const errors = [];
  if (!isValidAmount(payment.amount)) errors.push('Invalid amount');
  if (!isValidPaymentMethod(payment.method)) errors.push('Unsupported payment method');

  const methodErrors = validatePaymentMethod(payment);
  errors.push(...methodErrors);

  if (errors.length > 0) return { isValid: false, errors, processedAmount: 0 };

  const fee = getTransactionFee(payment.method);
  return {
    isValid: true,
    errors: [],
    processedAmount: payment.amount * fee
  };
}

function isValidAmount(amount) {
  return amount > 0;
}

function isValidPaymentMethod(method) {
  return ['credit_card', 'paypal'].includes(method);
}

function validatePaymentMethod(payment) {
  const validators = {
    credit_card: (p) => {
      const errors = [];
      if (!p.cardNumber || p.cardNumber.length !== 16) errors.push('Invalid card number');
      if (!p.expiryDate || p.expiryDate <= new Date()) errors.push('Card expired');
      if (!p.cvv || p.cvv.length !== 3) errors.push('Invalid CVV');
      return errors;
    },
    paypal: (p) => {
      if (!p.email || !p.email.includes('@')) return ['Invalid email'];
      return [];
    }
  };
  return validators[payment.method]?.(payment) || ['Unsupported payment method'];
}

function getTransactionFee(method) {
  const fees = { credit_card: 1.03, paypal: 1.02 };
  return fees[method] || 1;
}

Comparação de métricas:
- Complexidade ciclomática: 45 → 12 (redução de 73%)
- Linhas de código por função: 40 → 10 (média)
- Cobertura de testes possível: 20% → 95%
- Tempo estimado para entender o fluxo: 15 min → 2 min

A refatoração incremental, combinando extração de métodos, cláusulas de guarda e tabelas de decisão, transforma código de difícil manutenção em módulos coesos e testáveis. A chave é medir consistentemente, refatorar em pequenos passos e proteger cada mudança com testes unitários.

Referências