Engenharia de Software

Complexidade ciclomática, análise estática e refatoração

Complexidade ciclomática é uma métrica do campo da engenharia de software, desenvolvida por Thomas J. McCabe em 1976, e serve para mensurar a complexidade de um determinado módulo (uma classe, um método, uma função etc), a partir da contagem do número de caminhos independentes que ele pode executar até o seu fim. Um caminho independente é aquele que apresenta pelo menos uma nova condição (possibilidade de desvio de fluxo) ou um novo conjunto de comandos a serem executados.

As métricas de software podem ser definidas como métodos de determinar quantitativamente a extensão em que o projeto, o processo e o produto de software têm certos atributos. Isto inclui a fórmula para determinar o valor da métrica como também sua forma de apresentação e as diretrizes de utilização e interpretação dos resultados obtidos no contexto do ambiente de desenvolvimento de software. FERNANDES, A. A. (1995) – Gerência de Software através de métricas, São Paulo, Editora Atlas.

O resultado da complexidade ciclomática indica quantos testes (pelo menos) precisam ser executados para que se verifique todos os fluxos possíveis que o código pode tomar, a fim de garantir uma completa cobertura de testes.

Calculando a complexidade ciclomática

Existem diferentes formas de se calcular:

  • Usando a notação de um grafo de fluxo;
  • Usando fluxograma;
  • Com análise estática do código, usando uma ferramenta que automatize essa tarefa.

Tendo um grafo de fluxo ou um fluxograma, temos três fórmulas equivalentes para se mensurar a complexidade ciclomática:

  1. V(G) = R – onde R é o número de regiões do grafo de fluxo.
  2. V(G) = E – N + 2 – onde E é o número de arestas (setas) e N é o número de nós do grafo G.
  3. V(G) = P + 1 – onde P é o número de nós-predicados contidos no grafo G (só funciona se os nós-predicado tiverem no máximo duas arestas saindo.)

Ps: Nós-predicado são àqueles que podem desviar o fluxo da execução: if, while, switch etc.

Exemplo usando a notação de grafo de fluxo:

alt

Exemplo retirado do trabalho da professora Bianca Zadrozny (do Instituto de Computação da UFF), que pode ser consultado aqui como excelente referência.

A complexidade ciclomática desse código é 6. Esse resultado pode ser obtido usando uma das três formas acima descritas:

1) V(G) = R – onde R é o número de regiões do grafo de fluxo.

alt

Temos 6 regiões.

2) V(G) = E – N + 2 – onde E é o número de arestas (setas) e N é o número de nós do grafo G.

V(G) = 17 arestas/setas – 13 nós + 2 = 6

3) V(G) = P + 1 – onde P é o número de nós-predicados contidos no grafo G.

V(G) = 5 nós-predicados + 1 = 6

No final temos, então, 6 caminhos independentes. Com isso, sabemos que precisamos ter uma gama de pelo menos 6 testes para garantir uma boa cobertura para esse código.

Os seis caminhos independentes são:

  • 1) 1-2-10-12-13
  • 2) 1-2-10-11-13
  • 3) 1-2-3-10-11-13
  • 4) 1-2-3-4-5-8-9-2-[…]
  • 5) 1-2-3-4-5-6-8-9-2-[…]
  • 6) 1-2-3-4-5-6-7-8-9-2-[…]

Você pode traçar mentalmente um a um. Por exemplo, o primeiro caminho:

  • 1-2-10-12-13

alt

Com os caminhos definidos, você pode planejar os casos de teste para cada um deles.

Shell Script Básico
Curso de Shell Script Básico
CONHEÇA O CURSO

Ferramentas para análise estática

Essa é, claro, a mais produtiva forma: usar um analisador estático. Dentre as várias vantagens, temos:

  • Precisão;
  • Possibilidade de incluir a análise na integração contínua;
  • Geração de relatórios da evolução das métricas;

Existem várias ferramentas, você pode pesquisar sobre “análise estática” na linguagem que você utiliza em seus projetos, certamente encontrará várias opções (com focos variados).

Uma ferramenta bastante utilizada é a Sonar. Ela suporta mais de 25 linguagens. Tem a premissa de avaliar “débito de código” (o “preço” que se paga no futuro por algo que foi implementado no presente sem se preocupar tanto com a qualidade).

Se você desenvolve em PHP, escrevi um artigo sobre Ferramenta para avaliar a complexidade de código escrito em PHP.

Se você desenvolve em .NET temos:

  • ReSharper (Oferece muito mais que só análise estática)
  • ndepend (Tem um foco maior nas relações entre os objetos, acoplamento etc)

Quais os parâmetros aceitáveis para a complexidade dos meus métodos?

De acordo com o trabalho de McCabe, os valores de referência são:

ComplexidadeAvaliação
1-10Método simples. Baixo risco.
11-20Método razoavelmente complexo. Moderado risco.
21-50Método muito complexo. Elevado risco.
51-NMétodo de altíssimo risco e bastante instável.

Esses são apenas valores de referência. O fato de um método ter baixa complexidade não quer dizer que ele não pode ser melhorado ou até mesmo refatorado. Essa é a parte relativa da “coisa”. Caberá a você e a sua equipe identificar esses pontos.

Como posso melhorar os meus códigos?

Um código com baixa complexidade ciclomática não necessariamente já esgotou todas as possibilidades de melhorias. Há um conjunto de fatores envolvidos. Mas, em termos gerais, refatorar é a melhor “ferramenta” para se ter um código melhor autodocumentado e com menos complexidade.

Nem sempre conseguimos de “prima” acertar em cheio naquilo que desenvolvemos. Para preencher essa lacuna, existe a refatoração: melhorar a compreensão do código existente (sem alterar o seu comportamento).

Martin Fowler criou um catálogo com algumas técnicas de refatoração. Elas são focadas em se ter um código autodocumentado (expressivo, de fácil leitura, coeso etc).

Algumas das principais técnicas descritas:

Substituir recursividade por iteração: O código recursivo é difícil de entender e na maioria dos casos pode ser substituído por iteração. A não ser que se tenha um objetivo muito bem definido para a existência da recursão (para se ter algum expressivo ganho em performance; ou se você está trabalhando com uma operação muito complexa com árvores binárias etc).

Exemplo (baseado no do catálogo):

public void countDown(int n) {
    if(n == 0) return;

    System.out.println(n + "...");
    waitASecond();
    countDown(n-1);
}

Poderia ser escrito assim:

public void countDown(int n) {
    while(n > 0) {
        System.out.println(n + "...");
        waitASecond();
        n -= 1;
    }
}

Decompor condicional: Quando se têm uma estrutura condicional muito complicada com diversos if-elseif-else, pode-se extrair métodos dos resultados dessas condições e invocá-los.

Exemplo (baseado no do catálogo):

if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
    charge = quantity * _winterRate + _winterServiceCharge;
} else {
    charge = quantity * _summerRate;
}

Poderia ser decomposto para:

if (notSummer(date)) {
  charge = winterCharge(quantity);
} else {
  charge = summerCharge(quantity);
}

Substituir ninhos de condicionais por cláusulas de proteção: Para tirar a complexidade do entendimento do caminho de execução.

Exemplo (baseado no do catálogo):

double getPayAmount() {
  double result;
  if (_isDead) result = deadAmount();
  else {
    if (_isSeparated) result = separatedAmount();
    else {
      if (_isRetired) result = retiredAmount();
      else result = normalPayAmount();
    };
  }
  return result;
};

Poderia ser escrito assim:

double getPayAmount() {
  if (_isDead) return deadAmount();
  if (_isSeparated) return separatedAmount();
  if (_isRetired) return retiredAmount();
  return normalPayAmount();
};

Substituir condicional por polimorfismo: Tendo um comando condicional que a partir do tipo de objeto escolhe diferentes comportamentos, você pode tornar o método original abstrato e mover essas condicionais para suas próprias subclasses.

Exemplo retirado do catálogo:

class Bird {
  //...
  double getSpeed() {
    switch (type) {
      case EUROPEAN:
        return getBaseSpeed();
      case AFRICAN:
        return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
      case NORWEGIAN_BLUE:
        return (isNailed) ? 0 : getBaseSpeed(voltage);
    }
    throw new RuntimeException("Should be unreachable");
  }
}

Poderia ser descomposto para:

abstract class Bird {
  //...
  abstract double getSpeed();
}

class European extends Bird {
  double getSpeed() {
    return getBaseSpeed();
  }
}

class African extends Bird {
  double getSpeed() {
    return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
  }
}

class NorwegianBlue extends Bird {
  double getSpeed() {
    return (isNailed) ? 0 : getBaseSpeed(voltage);
  }
}

// Somewhere in client code
speed = bird.getSpeed();

Outras técnicas com exemplos aplicados, você pode consultar aqui.

Focar na qualidade do meu software ou na entrega?

Ideologicamente falando, o processo de refatorar é contínuo. O software vai crescer, precisará ser “tocado” e a refatoração andará lado a lado a tudo isso. Não existe código perfeito, não existe código “à prova de bala”, mas é possível ter código coeso, fácil de ser lido e o principal: fácil de ser testado.

Aliar entrega (o resultado final, o que interessa para a empresa) com qualidade de software é uma questão ampla e complexa. Sabemos que, na maioria das vezes, a realidade é essa: pressão para que as entregas aconteçam o mais rápido possível. Quase nunca entra na análise de tempo e custo o que será gasto para cuidar da qualidade/análise do código. O foco é quase sempre nas entregas e, pensando no lado “corporativo”, elas são extremamente importantes para manter a competitividade da empresa e isso não pode ser ignorado, mas também tem o lado negativo, que é o débito de código (o custo e o tempo que será gasto no futuro para implementar algo novo ou corrigir uma implementação).

Não tem “receita de bolo”, não tem manual, a equipe precisa aprender a mensurar todas essas nuances e achar um “meio termo”. Não dá pra julgar essa guerra de interesses, todos têm seus motivos e suas justificativas.

Outro ponto interessante de reflexão: Não, ninguém usa todas as “melhores técnicas” de codificação (e isso é muito relativo). “Código impecável” e que usa as “melhores técnicas” etc, quase sempre é apenas case para palestrantes, professores e artigos (como este aqui). No entanto, isso não tira e nem invalida o fato de que, como desenvolvedores, podemos evoluir, podemos encontrar novas formas e novos conceitos de pensar no “todo” que envolve o desenvolvimento de software. E essa responsabilidade é nossa, não sendo possível delegá-la.

Concluindo

Os temas aqui introduzidos são vastos. A ideia não foi a de esgotar os assuntos (e isso nem seria possível), o objetivo principal foi o de acender àquela “chama” que carregamos em nós: àquela que alimenta o nosso amor e interesse pelo desenvolvimento de software. Temos de mantê-la acesa, estudando coisas novas e buscando um conhecimento multimodal.

Até a próxima!

DNS - Fundamentos
Curso de DNS - Fundamentos
CONHEÇA O CURSO

Não seja um profissional “intolerante”

Todo profissional da área de TI já viu ou até fez parte de uma discussão com algum amigo ou colega de trabalho sobre o quão uma determinada tecnologia/linguagem/plataforma pode ser melhor do que a outra. É fácil darmos exemplos deste tipo de discussão: “Java vs .NET”, “Windows vs Linux”, “Android vs iOS”… Os exemplos são vários.

Essa discussão, obviamente, é muito bem-vinda! Uma discussão saudável é um dos melhores meios para aprendermos coisas novas e mudarmos paradigmas de pensamento. Mas, e quando a discussão torna-se, digamos, “acalorada” demais? E quando alguém simplesmente diz que a tecnologia/linguagem/plataforma que a pessoa em questão utiliza é melhor que todas as outras simplesmente por que a pessoa diz que é assim e pronto? Nesse momento, estamos diante de um profissional intolerante, pelo menos tecnicamente falando: é o famoso “xiita”. O final de uma discussão com um profissional intolerante? Geralmente, o festival de frases como “funciona porque é Linux”, “Java é que é linguagem de gente grande”, e por aí vai.

Discussões com “xiitas” acabam em nada

Esse tipo de discussão sempre acaba mais no emocional. No final, acabamos por ter pouca argumentação técnica e muito achismo e convicção pessoal. Não é que cada um não possa ter sua opinião e defendê-la, mas estamos em uma área onde é necessário que se prove o que se fala para que se possa ter um juízo correto. Porém, o profissional que é intolerante quase nunca vai dar valor a uma argumentação realmente técnica e, no final, ainda vai ficar com a opinião de que “o que importa é o que ele fala”. Uma pena, pois talvez uma grande oportunidade de aprendizado pode ter sido deixada para trás.

Oportunidades são deixadas para trás

Essa falta de flexibilidade por muitas vezes faz o profissional perder verdadeiras oportunidades de aprendizado e crescimento. Imagine a seguinte situação: você tem a possibilidade de participar de um super projeto legal, envolvendo várias pessoas ao redor do mundo, em uma empresa mundialmente reconhecida na área de TI e com uma excelente remuneração… Enfim, uma oportunidade para mudar uma vida! O grande ponto é que você curte desenvolver em Java, mas o projeto é em… .NET. E agora? Você deixaria a oportunidade única passar ou aceitaria o desafio?

Parece ser uma decisão relativamente fácil de ser tomada, mas não pra quem é inflexível. E, por incrível que pareça, não seria difícil de acreditar que alguém recusou a oportunidade. “Ah, recusei porque era .NET, C# não presta.”… Certamente esse seria provavelmente uma resposta que poderíamos com uma certa facilidade ouvir de alguém que preferiu recusar a oportunidade em questão. Parece absurdo, não? E, de certa forma é, mas infelizmente esse exemplo de intransigência também é comum. Mas sabemos que oportunidades que passam por nós raramente voltam e o arrependimento pode bater depois…

Symfony - Gerenciando aplicações com Symfony Flex
Curso de Symfony - Gerenciando aplicações com Symfony Flex
CONHEÇA O CURSO
Tecnologia da Informação serve para agregar valor ao negócio

Aqui vale aquela máxima: Tecnologia da Informação serve para agregar valor ao negócio, os deixando mais rápidos, eficientes, dinâmicos e adequados à realidade de velocidade extrema que temos hoje em dia com o menor custo operacional. Esse é o objetivo de qualquer projeto de software, no final. E isso, muitas vezes, quer dizer que o cliente não quer saber muito se a solução a ser desenvolvida utilizará Java, .NET, Ruby ou até mesmo Cobol. Ele quer que a solução seja eficiente e se adeque à sua realidade de negócio. Não adianta, por exemplo, utilizar Ruby em uma empresa onde o Java predomina. E essa comparação serve para qualquer linguagem/framework/tecnologia.

A ferramenta a ser utilizada é quem tem que na maioria das vezes se adaptar ao cliente, e não o contrário. E, justamente pela heterogeneidade entre os diferentes ambientes de negócio, não dá para cravarmos uma “bala de prata” para todos os cenários. Existem clientes onde .NET será mais adequado, outros onde o Java será mais adequado, outros onde até o Delphi será mais adequado à realidade de negócio do cliente! Aqui, o “xiitismo” acaba não tendo muita vez…

Esse tipo de discussão é muito chato!

Convenhamos: esse tipo de discussão na verdade é improdutiva e chata demais, justamente porque não leva a lugar algum de fato. Fora que, de fora, os participantes acabam por parecer profissionais que não sabem lidar com opiniões e experiências divergentes… Isso quando a pessoa não chega a parecer de fato uma criança birrenta!
Ninguém é obrigado a aceitar a opinião de outra pessoa como se fosse a verdade absoluta (por isso que uma discussão sadia e produtiva é sempre muito bem-vinda!). Agora, temos que ter maturidade e aceitar o fato de que alguém pode ter experiências e ideais diferentes dos nossos. E o fato de alguém pensar diferente não significa que existe alguém inferior ou errado na história na maioria das vezes. São somente… Diferenças! Temos que aprender a conviver com elas. E isso vale também para nós como profissionais da área de TI.

Se as discussões são chatas, os envolvidos automaticamente passam a ser chatos também

Profissionais intolerantes são automaticamente classificados como chatos e até mesmo muitas vezes arrogantes. Isso acontece porque simplesmente ninguém consegue conviver com uma pessoa que é completamente intransigente e não aceita que podem existir outras maneiras de se enxergar as coisas. Quando há algum fator emocional envolvido, a convivência pode até ser tolerada. Mas, como estamos falando do âmbito profissional e de maneira específica à nossa área de TI (mesmo que isso tudo não sirva somente para quem trabalha com tecnologia), muitas vezes isso acaba não valendo.

A certeza na área de TI é muito temporal

Temos sempre que nos lembrar que a nossa área é completamente mutante. Tecnologias novas sempre estão surgindo, frameworks novos e diferentes são lançados todos os dias praticamente… Essa interessante mutabilidade (que é o que pra mim faz a nossa área ser tão legal) também traz uma consequência: praticamente nenhuma verdade é absoluta por muito tempo.

Linguagens e frameworks nascem e são deixados de lado praticamente com a mesma velocidade com que foram adotados. Até algum tempo atrás, ninguém pensava em utilizar JavaScript do lado do servidor, até aparecer o Node.js… Delphi era considerada uma linguagem moderna e de ponta, hoje nem sequer é considerada (para mim, em alguns cenários, de maneira injusta) ao se desenvolver um novo projeto… Ninguém pensava em desenvolver aplicações mobile híbridas com HTML, CSS e JavaScript… Temos sempre que nos lembrar: o que sabemos hoje pode ser considerado completamente antiquado e ultrapassado daqui alguns anos. Sendo assim, pra quê perder tempo e energia com intransigência? Linguagens, frameworks e arquiteturas se foram, se vão e continuarão indo embora.

Cabe a nós nos desprendermos das convicções pessoais “intocáveis” e corrermos atrás.

Essa discussão não é uma bobeira ou qualquer coisa assim por envolver aspectos humanos

Infelizmente, é comum na área de TI encontrarmos pessoas que acham que basta ter um super conhecimento e ser alguém que manje horrores do aspecto técnico é mais do que suficiente. Mas não, isso não é suficiente.
Nós não somos máquinas, nem vivemos como se fôssemos. O ser humano é social por definição e, por isso, vive através de interações humanas. É impossível uma pessoa viver completamente reclusa de outras pessoas. Nós precisamos de contato para viver.

Se isso vale para a vida de maneira geral, por que não serviria para o aspecto profissional? Do que adianta termos um profissional super capacitado tecnicamente se este profissional não consegue se comunicar e interagir com os outros profissionais ao redor? A capacitação técnica, em algum momento, não vai ser suficiente por si só. Habilidades sociais hoje, além de necessárias como sempre foram, são também ferramentas profissionais. É preciso saber conversar, ouvir e conviver com outras pessoas, que por muitas vezes pensarão de maneira diferente e enxergarão as coisas por outra óptica. Isso é uma ferramenta de sobrevivência não somente profissional, mas também pessoal.

Mas as coisas não precisam ser chatas…

Não estou tentando pregar regras de convivência profissionais e pessoais, nem tenho esse direito, na verdade. Muito menos estou aqui para julgar. Isso é apenas uma reflexão sobre uma situação muito comum na área de Tecnologia da Informação. Lógico que aquela tiração de sarro e aquela “zueira” com o amigo que programa em alguma linguagem ou utiliza algum framework é super bem-vinda. Lógico que nós podemos ter a nossa opinião, inclusive temos o direito (e até mesmo o dever) de defendê-la. Nós, por muitas vezes, também acabamos por ser intransigentes e passamos dos limites… E isso é normal.

Aqui, a intenção foi falar sobre aquelas pessoas que adotam a intolerância como uma filosofia de vida. Falamos sobre pessoas que acham que ninguém pode pensar de maneira diferente e/ou discordar. E o ponto maior não é a pessoa ser chata ou qualquer coisa assim: o cerne da questão é a pessoa não perceber que na verdade ela está se prejudicando… O grande ponto é a pessoa não perceber que ela poderia ser um profissional muito melhor se deixasse essa falta de flexibilidade de lado e abrisse a mente para tudo que ela poderia aprender se ouvisse com mais atenção o que os outros têm a dizer! o/

E você? Concorda com este post? Já passou por alguma situação onde já precisou trabalhar com um profissional intransigente? Sinta-se à vontade para comentar com a gente nos comentários! =)

Composer - Gerenciador de dependências para PHP
Curso de Composer - Gerenciador de dependências para PHP
CONHEÇA O CURSO