amadeu josé freitas barroso andrade · testes, mas o foco deste projeto foram os testes de...
TRANSCRIPT
Universidade do MinhoEscola de Engenharia
Amadeu José Freitas Barroso Andrade
outubro de 2015
Análise de complexidade de programas emferramentas de apoio à decisão
Amad
eu J
osé
Frei
tas
Bar
roso
And
rade
An
ális
e d
e c
om
ple
xid
ad
e d
e p
rog
ram
as
em
fe
rra
me
nta
s d
e a
po
io à
de
cisã
oU
Min
ho|2
015
Amadeu José Freitas Barroso Andrade
outubro de 2015
Análise de complexidade de programas emferramentas de apoio à decisão
Universidade do MinhoEscola de Engenharia
Trabalho efetuado sob orientação do Professor Doutor Cláudio Manuel Martins Alves
Dissertação de MestradoMestrado em Engenharia de Sistemas
iii
AGRADECIMENTOS
Deixo o meu agradecimento a todas pessoas que tornaram este projeto possível, em especial:
À minha família, que sempre me apoiou.
Ao meu orientador, o professor Cláudio Alves, pela oportunidade de trabalhar sob a sua orientação e
por todo o tempo que dedicou a aconselhar-me.
À SISCOG, pela oportunidade de realizar este projeto com o seu apoio.
Ao Ricardo Nogueira, por ter sido o impulsionador deste projeto e ter-se mostrado sempre disponível
para ajudar.
A todos os meus amigos que estiveram comigo durante este trajeto.
v
RESUMO
Na área do desenvolvimento de software, o processo de análise de código é um processo
extremamente delicado, pois é suscetível a erros e que varia consoante a experiência do programador,
tornando-se por vezes um processo complexo e demorado se for feito sem a ajuda de ferramentas
informáticas.
O trabalho desenvolvido nesta dissertação usou a linguagem LISP como base e visa proporcionar uma
nova forma de apoio aos analistas-programadores durante a análise do código produzido e também
para servir de apoio à equipa de testes na contabilização do número de casos de testes a desenhar.
Aqui é também apresentado um estudo sobre todos os conceitos teóricos relativos à complexidade
ciclomática e tudo que esta envolve, fazendo a ligação desta métrica de análise com os testes de
software e os grafos de controlo de fluxo.
Palavras-Chave: Complexidade ciclomática, testes de software, grafos de controlo de fluxo, análise de
software.
vii
ABSTRACT
In the field of software development, the process of code analysis is a very delicate process because it
is error-prone and varies with the experience of the developer, making it a complex and time consuming
process if done without the support of the appropriate software tools.
The work described in this thesis was done using LISP and aims to provide a new form of support for
the anlyst-programmer during the analysis of the code produced and also to provide support to the tests
team making it possible to know the number of test cases that need to be designed.
It is also presented a study of all the theoretical concepts related to cyclomatic complexity and all the
related issues, linking this analysis metric with the software testing process and with the control flow
graphs.
KEYWORDS: Cyclomatic complexity, software testing, control flow graphs, software analysis.
ix
ÍNDICE
1. Introdução ................................................................................................................................... 1
1.1 Contextualização e Enquadramento ........................................................................................ 1
1.2 Motivação e objetivos ............................................................................................................. 3
1.3 Apresentação da empresa ...................................................................................................... 3
1.4 Estrutura da dissertação ........................................................................................................ 5
2. Complexidade ciclomática de programas ..................................................................................... 7
2.1 Complexidade em geral ......................................................................................................... 7
2.2 Definição e caracterização da complexidade ciclomática ......................................................... 8
2.3 Aspetos positivos da complexidade ciclomática ..................................................................... 15
2.4 Aspetos negativos da complexidade ciclomática ................................................................... 16
3. Testes de software ..................................................................................................................... 19
3.1 Introdução aos testes de software ........................................................................................ 19
3.2 Testes de caixa branca e testes de caixa preta ..................................................................... 19
3.3 Testes e complexidade ciclomática ....................................................................................... 21
4. Grafos de controlo de fluxos ....................................................................................................... 23
4.1 Introdução à teoria de grafos e grafos de controlo de fluxo .................................................... 23
4.1.1 Conceitos da teoria de grafos ........................................................................................ 24
4.2 Complexidade ciclomática e os grafos .................................................................................. 25
5. Implementação .......................................................................................................................... 29
5.1 Abordagens ponderadas ...................................................................................................... 29
5.2 Linguagem de programação e ferramentas externas de apoio ............................................... 29
5.2.1 LISP ............................................................................................................................. 30
5.2.2 GNU Emacs e SLIME .................................................................................................... 31
5.2.3 Allegro CL IDE .............................................................................................................. 31
5.2.4 Analisador de código: code walker ................................................................................. 31
5.2.5 Gestor de bibliotecas LISP: Quicklisp ............................................................................. 33
5.2.6 Biblioteca de grafos: CL-Graph ...................................................................................... 34
5.2.7 Graphviz ....................................................................................................................... 34
x
5.3 Estrutura do código .............................................................................................................. 35
5.4 Decisões de implementação ................................................................................................ 36
5.4.1 Definição genérica ........................................................................................................ 36
5.4.2 Operador lógico: and e or .............................................................................................. 36
5.4.3 Condicional simples: if .................................................................................................. 38
5.4.4 Condicional simples: cond ............................................................................................. 39
5.4.5 Condicional simples: when e unless .............................................................................. 40
5.4.6 Condicionais compostos ............................................................................................... 41
5.4.7 Iterador: dolist .............................................................................................................. 43
5.4.8 Iterador: do/do* ............................................................................................................ 45
5.4.9 Operadores especiais: let e let* ..................................................................................... 47
5.4.10 Operadores especiais: labels e flet .............................................................................. 48
5.4.11 Operadores especiais: multiple-value-bind ................................................................... 49
5.4.12 Outros ........................................................................................................................ 49
5.5 Relatório .............................................................................................................................. 49
6. Estudo de casos ........................................................................................................................ 51
6.1 Caso I – Definição de cálculo de compensações ................................................................... 51
6.2 Caso II – Definição de expansão de partilha de equipamento ................................................ 53
6.3 Caso III – Definição de processamento de licitações ............................................................. 56
7. Conclusões ................................................................................................................................ 61
7.1 Síntese dos resultados alcançados ....................................................................................... 61
7.2 Trabalho futuro .................................................................................................................... 63
Bibliografia ....................................................................................................................................... 65
Anexo I – Relátorio gerado para o caso I ............................................................................................ 73
Anexo II – Grafo do caso de estudo II ................................................................................................ 74
Anexo III – Grafo do caso de estudo III (original) ................................................................................ 77
Anexo IV – Grafo do caso de estudo III (refatorizado) ......................................................................... 79
xi
LISTA DE FIGURAS
Figura 1 - Logótipo da SISCOG. ........................................................................................................... 4
Figura 2 - Exemplo de um if. ............................................................................................................. 10
Figura 3 - Exemplo de grafo de controlo de fluxo. ............................................................................... 11
Figura 4 - Grafo de controlo de fluxo da métrica original. .................................................................... 11
Figura 5 - Grafo de controlo de fluxo com a alternativa 1. ................................................................... 12
Figura 6 - Grafo de controlo de fluxo com a alternativa 2. ................................................................... 13
Figura 7 - Linguagens de programação usadas na SISCOG. (SISCOG, 2013). ..................................... 30
Figura 8 - Exemplo de um if e um cond em LISP. .............................................................................. 32
Figura 9 - Exemplo de um macroexpand. ........................................................................................... 32
Figura 10 - Resultado do macroexpand. ............................................................................................. 32
Figura 11 - Exemplo de um grafo em linguagem DOT. ....................................................................... 34
Figura 12 - Estrutura geral do código implementado. ......................................................................... 35
Figura 13 - Grafo de um and/or. ....................................................................................................... 37
Figura 14 - Grafo de um and/or com nodos intermédios. ................................................................... 38
Figura 15 - Grafo de um if simples. ................................................................................................... 39
Figura 16 - Grafos da macro cond. .................................................................................................... 40
Figura 17 - Grafo de um when/unless. .............................................................................................. 41
Figura 18 - Pseudo-código de um if com recurso aos operadores and/or. .......................................... 41
Figura 19 - Grafos de um if com recurso aos operadores and/or. ...................................................... 42
Figura 20 - Grafos do dolist. .............................................................................................................. 44
Figura 21 - Grafo de um do/do*. ....................................................................................................... 45
Figura 22 - Grafo 2 de um do/do*. .................................................................................................... 46
Figura 23 - Grafo de um let/let*. ....................................................................................................... 47
Figura 24 – Definição do cálculo de compensações. .......................................................................... 52
Figura 25 - Grafo do caso de estudo I. ............................................................................................... 52
Figura 26 - Exemplo de parte do grafo do caso de estudo II. .............................................................. 55
Figura 27 - Condições do when no caso de estudo II. ........................................................................ 56
Figura 28 - Exemplo de parte do grafo da definição original do caso III. .............................................. 58
Figura 29 - Grafos das definições criadas. ......................................................................................... 59
Figura 30 – Anexo I: Exemplo de um dos relatórios HTML gerados. ................................................... 73
xii
Figura 31 - Anexo II - Grafo do caso de estudo II (Parte 1). ................................................................. 74
Figura 32 - Anexo II - Grafo do caso de estudo II (Parte 2). ................................................................. 75
Figura 33 - Anexo II - Grafo do caso de estudo II (Parte 3). ................................................................. 76
Figura 34 - Anexo III - Grafo do caso de estudo III, original (Parte 1). .................................................. 77
Figura 35 - Anexo III: Grafo do caso de estudo III, original (Parte 2). ................................................... 78
Figura 36 - Anexo IV - Grafo do caso de estudo III, refatorizada (Parte 1). ........................................... 79
Figura 37 - Anexo IV - Grafo do caso de estudo III, refatorizada (Parte 2). ........................................... 80
xiii
LISTA DE TABELAS
Tabela 1 - Fórmulas da complexidade ciclomática. ............................................................................ 10
Tabela 2 - Análise do valor da complexidade ciclomática (Enescu, Mancas, Manole, & Udristoiu, 2008).
........................................................................................................................................................ 13
Tabela 3 - Ferramentas utilizadas no projeto. ..................................................................................... 30
Tabela 4 - Formas retornadas pelo code walker. ................................................................................ 33
Tabela 5 - Grupos de palavras reservadas. ........................................................................................ 36
Tabela 6 - Resumo sobre o cálculo da complexidade das novas definições. ........................................ 59
xv
LISTA DE ABREVIATURAS, SIGLAS E ACRÓNIMOS
AAAI - American Association for Artificial Intelligence
AST - Abstract Syntax Tree
IDE – Integrated Development Environment
IEEE – Institute of Electrical and Electronics Engineers
1
1. INTRODUÇÃO
1.1 Contextualização e Enquadramento
O trabalho realizado e que irá ser descrito nesta dissertação, foi um projeto realizado numa empresa
portuguesa, SISCOG, na área de desenvolvimento de sistemas de apoio à decisão. Um dos objetivos
centrais deste projeto foi a construção de uma ferramenta que permitisse a análise automática da
complexidade de programas de código LISP, sendo um passo no processo de melhoria contínua pelo
qual a empresa se rege.
O trabalho desenvolvido nesta dissertação visa proporcionar uma nova forma de apoio aos
analistas-programadores durante a análise do código produzido e também para servir de apoio à
equipa de testes na contabilização do número de casos de testes a desenhar, pois é uma área onde é
gasta uma grande quantidade de esforço dos recursos da empresa.
Partindo dos testes de software em ferramentas de apoio à decisão, na engenharia de
software, os testes são, tradicionalmente, uma das principais técnicas a contribuir para uma maior
fiabilidade e qualidade do software (Del Grosso, Antoniol, Merlo, & Galinier, 2008), sendo por isso
usadas em várias empresas de desenvolvimento de software. Estes testes podem incidir numa vertente
de análise de eficiência ou numa vertente de resolução e/ou prevenção de erros. Neste projecto, o foco
principal foi a segunda vertente pois é aquela em que existe um maior número de recursos da empresa
envolvidos. Na procura do erro são desenhados casos de teste na tentativa de cobrir o máximo possível
de funcionalidades de um programa. Um caso de teste é considerado bom quando tem uma alta
probabilidade de mostrar um erro não descoberto até ao momento. Erro esse que pode ser
categorizado como erro fatal, que bloqueia por completo o funcionamento do programa, ou ser apenas
uma falha subtil, que origina uma alteração do output, sem causar a falha do programa (Kanewala &
Bieman, 2014). Se o teste não encontra erros serve para demonstrar o âmbito no qual o programa
funciona e serve portanto para validar que o programa cumpre os requisitos. Existem vários tipos de
testes, mas o foco deste projeto foram os testes de caixa-branca e os testes de caixa-preta, pois são os
mais usados na SISCOG.
Os testes de caixa-preta são tipicamente utilizados para isolar possíveis comportamentos
erróneos do sistema, tais como resultados, interfaces e rendimento. Por seu lado, os testes de caixa-
branca, são utilizados para verificar o software antes de integrá-lo no sistema, sendo baseados em
2
estruturas de controlo e correcção algorítmica. Nos testes de caixa-branca surge o método dos
caminhos básicos que engloba um grafo de fluxo e a sua complexidade ciclomática.
No que toca à complexidade de programas e sua medição, pode-se começar por referir que a
complexidade do software é sempre indesejada, já que é uma das razões fundamentais para o
diminuição da qualidade do software (Madi, Zein, & Kadry, 2013) e está directamente ligada à
produtividade dos recursos que trabalham na manutenção do software (Gill & Kemerer, 1991), pois
quanto mais complexo é um sistema, mais difícil é de manter, o que por sua vez vai acarretar custos
maiores (Gill & Kemerer, 1991; Coleman, Ash, Lowther, & Oman, 1994) para as empresas.
Neste contexto, surgem então as métricas de complexidade de software. Desde 1976 têm sido
usadas diferentes métricas para avaliar a complexidade de um sistema, acompanhar o progresso e
avaliar a efetividade do software (Suresh, Pati, & Rath, 2012; Fenton & Neil, 1999). Estas fornecem
uma forma de descrever quantitativamente os projetos de software, assim como uma forma de avaliar
os métodos e ferramentas usadas nesses projetos, com o intuito de aumentar a produtividade e
qualidade dos mesmos (Gill & Kemerer, 1991). Neste conjunto de métricas destaca-se a complexidade
ciclomática de McCabe. Esta métrica é considerada como um dos melhores indicadores da fiabilidade
do sistema (Suresh et al., 2012), sendo por isso escolhida como o principal foco desta dissertação.
A complexidade ciclomática surgiu em 1976 (McCabe, 1976), como uma abordagem para a
medição da complexidade de um sistema, através do cálculo do seu valor ciclomático. Para isso foram
necessárias algumas definições e teoremas da teoria de grafos, sendo o valor da complexidade
ciclomática calculado através do número de arestas e nós de um grafo de controlo de fluxo (Suresh et
al., 2012). O seu principal propósito, segundo Gill and Kemerer (1991), é identificar módulos de
software que vão ser difíceis de testar ou manter. Além desta avaliação da complexidade dos módulos
de um sistema, a complexidade ciclomática pode também ser usada para estimar o número de casos
de teste necessários para atingir uma máxima cobertura do código (Suresh et al., 2012), estudando
para isso o número de caminhos possíveis de um programa. No estudo de McCabe (1976), este refere
que apesar de ser possível definir um conjunto de expressões algébricas que fornecessem o número
total de caminhos possíveis através de um programa estruturado, usar este número não se tornaria
prático, optando-se então por seguir o método dos caminhos básicos, que quando combinados
gerariam todos os caminhos possíveis.
Estas informações foram o ponto de partida deste projecto, que teve como resultado final a
criação de uma ferramenta automática de análise de complexidade.
3
1.2 Motivação e objetivos
No desenvolvimento de software, a análise do código produzido ou existente é um processo suscetível
a erros e que varia com a experiência do programador, tornando-se por vezes num processo complexo
se for feito sem a ajuda de ferramentas informáticas.
O objetivo deste projeto centrou-se na análise automática da complexidade de ferramentas
informáticas dedicadas especificamente ao apoio à decisão e teve início com uma pesquisa
bibliográfica, em que se realizou um levantamento dos conceitos chave para a resolução do problema e
das principais técnicas e ferramentas que poderiam servir de suporte para a realização do mesmo.
Como este projeto pretendeu-se em particular avaliar a estrutura de ferramentas informáticas
através da medida da sua complexidade ciclomática. Usando como base programas reais em uso
numa empresa do sector, foi desenvolvida uma ferramenta de avaliação automática. A análise dos
resultados obtidos com esta ferramenta permite avaliar o número de casos necessários para testar
efetivamente estes programas, assim como avaliar quantitativamente a qualidade de partes do
software produzidas pela empresa.
A motivação para este estudo assentou na sofisticação de algumas das rotinas (nomeadamente
as de otimização) que compõem estas ferramentas, na dificuldade em delinear processos de teste
efetivos, no reduzido número de estudos dedicados a este assunto na literatura, e na criticidade dos
erros que podem ocorrer neste tipo de ferramentas.
1.3 Apresentação da empresa
A SISCOG é uma empresa criada em 1986, dedicada ao desenvolvimento de software, que fornece
sistemas de apoio à decisão com o intuito de otimizar o planeamento de recursos e gestão de
companhias de transporte, com um maior ênfase na ferrovia e metropolitanos.
Utilizando uma combinação de técnicas de inteligência artificial e investigação operacional
(Morgado & Martins, 1998; Morgado, Martins, & Haugen, 2003), a empresa foi criando e
desenvolvendo os seus produtos, que neste momento são o CREWS, o ONTIME e o FLEET.
Estes produtos surgiram na tentativa de cobrir grande parte do processo de planeamento e
gestão dos clientes, fornecendo ferramentas para que estes consigam otimizar de forma precisa,
efetiva e rápida os seus recursos operacionais e trabalho diário.
4
O CREWS é o produto usado no planeamento e gestão de pessoal, oferecendo aos utilizadores
diferentes níveis de suporte à decisão, como o modo manual, que apenas valida todas as restrições do
problema e faz os cálculos necessários enquanto os utilizadores constroem os planos dos recursos, o
modo semi-automático, que aponta uma direção para atingir uma boa solução e por fim, o modo
automático, que fornece uma solução otimizada. Este produto, foi premiado em 1997 e 2003 com o
"Innovative Application Award" dado pela AAAI e laureados pelo "The Computerworld Honors Program"
em 2006.
O ONTIME é usado para a criação de horários e cobre todo este processo, que vai desde a
criação de horários anuais até aos ajustes feitos no dia-a-dia. Neste produto é possível planear e gerir a
alocação de dois importantes recursos para as viagens das companhias: espaço (rotas, linhas, etc) e
tempo (tempo de saída e chegada).
O FLEET, por sua vez, é usado para o planeamento e gestão de material motor. Este cria o
escalonamento otimizado para veículos, considerando o número de passageiros expectável,
especificações da frota e restrições operacionais. Produz também planos cíclicos de longo-prazo,
planos de calendário de curto-prazo, lida com a manutenção planeada de veículos e fornece suporte à
decisão para as operações do dia-a-dia.
Relativamente aos produtos FLEET e ONTIME, a empresa encontra-se neste momento a fazer
grandes desenvolvimentos.
Figura 1 - Logótipo da SISCOG.
5
1.4 Estrutura da dissertação
Esta dissertação está dividida em 7 capítulos, que serão descritos em seguida.
No primeiro capítulo é feita uma introdução onde é apresentada a contextualização e
enquadramento do tema, é feita a apresentação da empresa, assim como a motivação e os objetivos
do projeto. Este capítulo acaba com a descrição da estrutura do relatório.
No segundo capítulo é introduzida a métrica da complexidade ciclomática, sendo exposto o
estudo geral feito sobre o tema. Este estudo é o resultado da revisão de literatura existente e cobre
aspectos como a sua definição, utilização, cálculo e análise dos seus resultados. São também
apresentadas algumas críticas a esta métrica.
No terceiro capítulo é introduzido o tema dos testes de software, onde são abordados os testes
de caixa branca e caixa preta, assim como a sua ligação à complexidade ciclomática.
No quarto capítulo é apresentada uma revisão sobre os grafos de controlo de fluxo, começando
por uma breve introdução à teoria de grafos direcionada para o número ciclomático e acabando na
ligação entre os grafos de controlo de fluxo e a complexidade ciclomática.
No quinto capítulo é apresentado o processo de desenvolvimento da ferramenta informática,
desde a linguagem de programação utilizada até aos elementos principais e cálculos efetuados pela
ferramenta.
No sexto capítulo mostram-se exemplos práticos de utilização da ferramenta, apresentando e
analisando os casos de estudos selecionados.
No último capítulo são retiradas conclusões do trabalho realizado e apresentam-se propostas
para trabalho futuro.
7
2. COMPLEXIDADE CICLOMÁTICA DE PROGRAMAS
Neste capítulo será exposto o estudo geral feito sobre a métrica da complexidade ciclomática. Este
estudo é o resultado da revisão de literatura existente e cobre aspectos como a sua definição, críticas,
utilização, cálculo e análise dos resultados.
2.1 Complexidade em geral
Antes de abordar a parte da complexidade ciclomática, vou começar por introduzir o tema da
complexidade em geral, passando uma visão geral sobre o que é a complexidade de software e como é
que esta tem vindo a ser definida na engenharia de software.
A complexidade de software é tradicionalmente um indicador direto da qualidade e do custo do
software (Banker, Srikant, Kemerer, & Zweig, 1993; Basili & Perricone, 1983; Curtis, Sheppard, &
Milliman, 1979; Gill & Kemerer, 1991; Munson & Khoshgoftaar, 1992; Wilkie & Hylands, 1998; Jay et
all., 2009), pois quanto maior a complexidade de um programa, maior é a probabilidade de este ter
falhas, o que consequentemente se traduz numa menor eficiência do mesmo (Gaur, 2013; Khalid, ul
Haq, & Khan, 2013; Yadav & Khan, 2012; Bandara, Wikramanayake, & Goonethillake, 2009).
Na engenharia de software, o conceito genérico de complexidade foi investigado por vários
autores, resultando daí uma grande variedade de definições que correspondem às diferentes visões de
complexidade dos mesmos. De seguida irei expor alguns conceitos, compilados no artigo de Abram,
Lopez, and Habra (2004):
Em IEEE (1990), pode-se encontrar o conceito de complexidade como sendo o grau de
dificuldade em entender e verificar o desenho ou implementação de um sistema ou componente,
sendo aqui definido que a complexidade é uma propriedade da implementação do desenho, mas que
está também relacionada com o esforço necessário para perceber e verificar a implementação do
desenho.
Em Evans and Marciniak (1987), a complexidade é baseada na estrututura do sistema e na
definição de algumas das suas caraterísticas, sendo então determinada por fatores intrínsecos ao
sistema, como o número e complexidade das interfaces, o número e complexidade dos ramos
condicionais, pelo grau de encadeamento e pelos tipos de estruturas de dados. Quanto maior o
número de fatores e relações de um sistema, maior é a interação entre os seus elementos, o que torna
o sistema mais complexo.
8
Em Whitmire (1997), o conceito de complexidade tem um âmbito mais abrangente, pois
considera alguns tipos de complexidade, como a complexidade computacional e psicológica. Estes
conceitos de complexidade computacional e complexidade psicológica, são baseadas nas noções
propostas por Whitmire (1997) e Henderson-Sellers (1996), sendo a primeira definida em termos de
recursos de hardware necessários para executar o software e, a segunda, baseada em fatores como o
problema complexo que foi resolvido, as caraterísticas do software utilizadas e o conhecimento e
experiência do programador sobre o problema e domínio da solução.
Analisando estas definições, pode-se concluir que a complexidade é algo que a indústria de
software deve evitar, sendo por isso a questão da avaliação da complexidade uma área crítica no
desenvolvimento do software. Nesta área é gasto muito esforço na tentativa de identificar técnicas e
métricas para avaliar a complexidade do software e dos seus módulos (Munson & Khoshgoftaar,
1989), mas que também permitam o acompanhamento do progresso e avaliação da efetividade do
software (Fenton & Neil, 1999). Estas métricas são então consideradas como fatores decisivos na
medição da qualidade de um produto de software.
Segundo Gill and Kemerer (1991), sem o recurso às métricas, as tarefas de planeamento e
controlo do desenvolvimento de software iriam permanecer estagnadas, já que estas competências são
adquiridas apenas através do ganho de experiência, e esta, não é facilmente transferível para um
próximo sistema ou para futuros melhoramentos. Por seu lado, com o uso das métricas, os projetos de
software podem ser quantitativamente descritos e os métodos e ferramentas usadas nesses projetos
para melhorar a produtividade e qualidade podem ser avaliados.
Sendo a métrica da complexidade ciclomática um dos melhores indicadores da fiabilidade de
um sistema (Suresh et al., 2012), irei aborda-la de seguida.
2.2 Definição e caracterização da complexidade ciclomática
Sabendo que os problemas de testabilidade e manutenção nasceram do fato de as empresas
passarem metade do tempo de desenvolvimento em testes (Boehm, 1973) e do custo excessivo em
manter os sistemas (Cammack & Rogers, 1973), a métrica da complexidade ciclomática surgiu como
uma técnica matemática que fornece uma base quantitativa para a modularização de um sistema de
software, permitindo identificar quais os módulos desse software é que vão ser difíceis de testar ou
manter (McCabe, 1976).
9
Em McCabe, Wallace and Watson (1996), esta métrica é descrita como tendo dois propósitos.
Um deles é ser usada durante todas as fases do ciclo de vida do software, na tentativa de manter o
software fiável, testável e gerível; o outro é dar o número recomendado de casos de teste para o
software. Pelo mesmo caminho, em Shuman (1990) e Suresh et al., (2012), a métrica da
complexidade ciclomática é descrita como fornecedora de um meio para quantificar a complexidade do
software e a sua utilidade tem vindo a ser sugerida no processo de desenvolvimento e de testes de
software.
Para o cálculo desta, é necessário recorrer à teoria de grafos (McCabe, 1976; McCabe et al.,
1996), pois esta métrica calcula a complexidade de um módulo baseada no seu grafo de controlo de
fluxo, isto é, todas as instruções de um módulo são transformadas em nodos do grafo, com os
caminhos do grafo a representarem a sua ligação/sequência, sendo depois representada por um único
número (Madi et al., 2013).
Sendo os grafos tão importantes para a medição da complexidade ciclomática, todos os
pormenores relativos a estes vão ser abordados num capítulo em separado (4. Grafos de controlo de
fluxos).
Originalmente, a fórmula de cálculo desta métrica, foi definida por McCabe (1976) como sendo:
V(G) = E – N + 2
Onde:
V(G), é o valor da complexidade do grafo G;
E, é o número de caminhos do grafo;
N, é o número de nodos.
A complexidade ciclomática pode também ser definida por meios alternativos, sendo as formas mais
usuais (McCabe, 1976; Sarwar, Ahmad, & Shahzad, 2012; Pressman, 2010; Jovanović, 2006) as
seguintes:
10
Fórmula Explicação
Original V(G) = E – N + 2 V(G), é o valor da complexidade
ciclomática do grafo G;
E, é o número de caminhos
existentes no grafo.
N é o número de nodos
existentes no grafo.
Alternativa 1 V(G) = P + 1 V(G), é o valor da complexidade
ciclomática do grafo G;
P é o números de pontos de
decisão do grafo.
Alternativa 2 V(G) = R V(G), é o valor da complexidade
ciclomática do grafo G;
R, é o número de regiões do
grafo (região exterior e regiões
interiores fechadas).
Tabela 1 - Fórmulas da complexidade ciclomática.
Segue um exemplo para ajudar na compreensão da Tabela 1, ilustrando assim todas as alternativas
possíveis para definir a complexidade ciclomática.
Usando um pequeno exemplo de um “if” simples:
Figura 2 - Exemplo de um if.
11
Começamos por desenhar o grafo de controlo de fluxo:
Figura 3 - Exemplo de grafo de controlo de fluxo.
Com a fórmula original, V(G) = E- N + 2, em que contamos o número de caminhos, representados na
Figura 4 por nodo origem à nodo destino, e o número de nodos, representados por um único número,
temos:
Figura 4 - Grafo de controlo de fluxo da métrica original.
12
V(G) = 5 – 5 + 2
V(G) = 2 à O valor da complexidade ciclomática é 2.
Com a alternativa 1, V(G) = P + 1, em que P é o número de pontos de decisão, ou seja, é a contagem
dos nodos onde existe uma condição, sendo estes caracterizados por terem 2 ou mais caminhos a sair
de si. Temos então:
Figura 5 - Grafo de controlo de fluxo com a alternativa 1.
V(G) = 1 + 1
V(G) = 2 à O valor da complexidade ciclomática é 2.
Por fim, com a alternativa 2, V(G) = R, em que R é o número de regiões. Quando se está a calcular
regiões, estas regiões são as áreas cercadas por caminhos e nodos (na Figura 6 denominada por
região fechada), e a área exterior ao grafo (na Figura 6 considerada região exterior):
13
Figura 6 - Grafo de controlo de fluxo com a alternativa 2.
V(G) = 1 + 1, em que somamos o número de regiões (a exterior e a fechada)
V(G) = 2 à O valor da complexidade ciclomática é 2.
Como podemos observar pelos exemplos em cima, independentemente da fórmula escolhida, o valor
da complexidade ciclomática é sempre o mesmo.
No que toca à análise dos resultados, a seguinte tabela mostra como é avaliado o risco de não se
conseguir testar todo o código, com base na complexidade ciclomática.
Complexidade ciclomática Avaliação do risco
1 – 10 Risco baixo; Código testável
11 – 20 Risco moderado
21 – 50 Risco alto
> 50 Risco muito alto; Código não testável
Tabela 2 - Análise do valor da complexidade ciclomática (Enescu, Mancas, Manole, & Udristoiu, 2008).
14
Segundo Enescu, Mancas, Manole, and Udristoiu (2009), analisando a Tabela 2, concluiu-se
que o número da complexidade ciclomática calculada de um programa deve estar no intervalo de 1 a
10 para o programa ser considerado simples e apenas aí o software é considerado como estando
quase livre de risco. Se o valor se encontra entre 11 e 20, então o risco é considerado moderado e o
programa começa a ser considerado como algo complexo. Se o número ciclomático cair no intervalo de
21 a 50, o módulo é considerado de alto risco, sendo o programa considerado muito complexo. Por
fim, se o valor ciclomático for maior que 50, o programa/módulo é considerado não testável e de
muito alto risco.
Em McCabe et al. (1996), foram analisadas as razões sobre o porquê de se dever limitar o
valor da complexidade ciclomática a 10, mas tal como em várias outras métricas da indústria de
software, não há um consenso quanto a um número fixo de complexidade ciclomática que sirva a
todas as empresas, no entanto, o valor de 10 é considerado como bom ponto de partida (McCabe,
1976).
Algumas das principais razões apontadas a favor do limite do valor da complexidade ciclomática
são que módulos excessivamente complexos são mais propensos a erros, são difíceis de perceber, são
mais difíceis de testar e também mais difíceis de modificar (Gaur, 2013; Khalid, ul Haq, & Khan, 2013;
Yadav & Khan, 2012; Bandara, Wikramanayake, & Goonethillake, 2009). Então, ao limitar a
complexidade em todas as fases de desenvolvimento de software, está-se a evitar os grande parte dos
problemas associados à alta complexidade de software.
Segundo McCabe et al. (1996), várias organizações conseguiram implementar limites à
complexidade como parte dos seus programas de software. No entanto, o número exacto a ser usado,
permanece ainda sem consenso. O limite original de 10 tem evidências de suporte significativas
(McCabe, 1976), mas anos mais tarde, na publicação de McCabe et al. (1996), foi reportado que
limitar esse número a 15 foi igualmente usado com sucesso. Segundo essa publicação, limitar o valor
acima de 10 deve ser reservado a projetos com, por exemplo, desenvolvedores experientes, onde
existe um desenho formal, uma linguagem de programação moderna, programação estruturada,
orientações de código e um plano de testes compreensivo.
A Hewlett Packard, pode ser usada como exemplo de uma empresa que usou e limitou o
número da complexidade ciclomática acima de 10, pois segundo Grady (1992), na Hewlett Packard,
qualquer módulo com complexidade acima de 16, deve ser redesenhado, mostrando que até esse
valor a complexidade é aceitável.
15
2.3 Aspetos positivos da complexidade ciclomática
A métrica da complexidade ciclomática sempre foi relacionada com qualidades de software (Nystedt,
1999) como a reutilização, manutenção (Geoffrey, 1991), segurança (Chowdhury, 2009), previsão de
falhas de software (Shimeall, 1990) e como um bom guia para o desenvolvimento de casos de teste
(McCabe, 1976).
Em Butler (1983), esta métrica é descrita como o melhor método para analisar e limitar a
complexidade de programas de software, sendo que ao contrário da maioria das métricas de
complexidade que são mais discriminatórias, esta é facilmente calculada por uma análise estática
(Fenton & Neil, 1999).
Existem vários estudos que reforçam os aspectos positivos da complexidade ciclomática.
Muitos deles focam-se na análise entre a correlação da complexidade ciclomática com a incidência de
erros no software, como são os casos de Walsh (1979), Schneidewind (1979), Ward (1989), Meals
(1981), Henry, Kafura and Harris (1981), Gollhofer, Shimeall and Leveson (1983) e Shuman (1990).
Nestes estudos podem ser encontrados vários aspectos positivos para a utilização da complexidade
ciclomática.
Em Walsh (1979), foram analisados oito módulos de um programa de larga escala com
funcionalidades relacionadas. Neste estudo, foi medida a relação entre a ocorrência de erros e o valor
da complexidade ciclomática. Para a realização do estudo, os módulos a serem testados foram
divididos em dois grupos, um com o valor de complexidade ciclomática menor que 10 e outro onde era
maior. O objetivo era comparar a incidência de erros entre os dois grupos. No fim, o autor conclui que
havia uma incidência de erros 21% maior no grupo com maior complexidade ciclomática.
Em Schneidewind (1979), foram analisados quatro programas, todos com um número
diferente de instruções, com o intuito de examinar a correlação entre a incidência de erros e a
complexidade ciclomática. Da análise da estrutura destes programas, o autor concluiu que quanto
maior a complexidade ciclomática, maior a ocorrência de erros.
Em Ward (1989), foram analisados dois programas de larga escala, com milhares de linhas de
código, para estudar a correlação entre erros e complexidade ciclomática. Neste estudo foi encontrada
uma correlação estatística de 0.8 (numa escala entre -1 e 1) entre a complexidade e a densidade dos
erros.
Em Meals (1981), foram usadas ferramentas automáticas de medição de complexidade, para
analisar três programas independentes e de larga escala. O objetivo era determinar se existia uma
correlação entre complexidade ciclomática e a história de erros conhecida. Dois desses três programas
16
mostraram uma correlação entre a incidência de erros e a complexidade ciclomática, apesar de não ter
sido dado nenhum coeficiente. A conclusão foi que a complexidade ciclomática é um indicador útil de
secções do código propensas a erros.
Em Henry, Kafura and Harris (1981), foi levado a cabo o estudo de um programa de larga
escala. Foi encontrada uma forte correlação, 0.95 (numa escala entre -1 e 1) entre procedimentos que
continham erros e a complexidade ciclomática.
Em Gollhofer, Shimeall and Leveson (1983), foi analisada a complexidade de 27 programas
independentes. Nove porcento dos módulos tinham complexidade superior a 10, sendo que estes
mesmos módulos tinham 47% do total de erros. Os autores concluiram que existe uma forte correlação
entre a complexidade ciclomática (quando maior que 10) e a incidência de erros.
Por fim, em Shuman (1990), foram analisadas múltiplas versões de programas relativamente
largos e complexos, sendo encontrada uma correlação entre erros e a medida de complexidade
ciclomática.
Como podemos observar por estes estudos, várias autores consideram existir uma forte
correlação entre a complexidade ciclomática e a incidência de erros.
Mais recentemente, em McCabe, McCabe Jr., and Fiondella (2012), os autores concluíram que
a complexidade ciclomática permite um escrutínio mais compreensivo da estrutura e do fluxo de
controlo de código, fornecendo capacidades de deteção significativamente melhores.
Mesmo sendo esta uma das métricas mais populares (Fenton & Neil, 1999; Sarwar, Shahzad,
& Ahmad, 2013) no que se refere à medição da complexidade de software, tem também algumas
críticas associadas que passarei a expor de seguida.
2.4 Aspetos negativos da complexidade ciclomática
A complexidade ciclomática tem vindo a sofrer alguns ajustes de modo a tornar-se uma métrica mais
completa e consensual (Curtis, 1983; Hansen, 1978; Harrison & Magel, 1981; Iyengar,
Parameswaran, & Fuller, 1982; Magel, 1981; Myers, 1977; Oviedo, 1980; Stetter, 1984; Woodward,
Hennell, & Hedley, 1979) mas, mesmo sendo esta bastante usada e amplamente citada tanto em
estudos, como artigos ou mesmo livros (Arthur, 1985; Cobb, 1978; De Marco, 1982; Dunsmore,
1984; Harrison, Magel, Kluczny, & De Kock, 1982; Schneidewind, 1979; Tanik, 1980, Pressman,
1987; Wiener & Sincovec, 1984), encontra-se também sujeita a várias críticas.
17
Em Jay et all. (2009), os autores concluíram que a complexidade ciclomática, por si só, não
tem poder explanatório suficiente, e que esta métrica mede a mesma propriedade que a métrica das
linhas de código. Vários estudos (Curtis, Sheppard, Milliman, Borst, & Love, 1979; Kitchenham, 1981;
Paige, 1980; Wang & Dunsmore, 1984; Basili & Hutchens, 1983) indicam mesmo que a métrica das
linhas de código tem melhor performance que a complexidade ciclomática.
Em Shepperd (1988), a complexidade ciclomática é colocada em causa tanto no campo
teórico como no campo empírico, sendo concluído que há uma grande dificuldade em avaliar a métrica
de McCabe e o trabalho empírico associado devido à inexistência de um modelo explícito onde a
complexidade ciclomática é baseada.
Em Baker & Zweben (1980), Oulsnam (1979) e Prather (1984), é argumentado que a
complexidade ciclomática de um programa pode aumentar quando são usadas técnicas na melhoria da
estrutura interna de um programa, sendo que essas técnicas são bastante consensuais na indústria de
software.
Em Garg (2014), o autor concluiu que apesar das grandes vantagens obtidas através da
análise do número da complexidade ciclomática, este não calcula o número exacto da complexidade do
software porque não tem em conta a interação entre duas classes de objetos.
Em Vinju and Godfrey (2012), foram analisados oito sistemas Java de código aberto e
recolhidas evidências empíricas, de que a complexidade ciclomática não avalia corretamente a
compreensibilidade dos métodos, pois tanto é possível que esta seja subestimada como sobrestimada.
Em Sarwar et al. (2013), é citado que a métrica de McCabe tem várias limitações, sendo um
dos principais problemas o encadeamento de condições. Neste estudo, é exemplificado que a
complexidade ciclomática calcula o mesmo valor de complexidade para uma construção simples e para
uma construção encadeada, sendo que no segundo caso o valor deveria ser maior já que o programa
se torna mais complexo.
Numa outra análise sobre o encadeamento de condições, em Solichah, Hamilton, Mursanto,
Ryan, and Perepletchikov (2013), os autores comentam que a complexidade ciclomática tem
incompletudes no que toca à medição da complexidade de um grafo de controlo de fluxo, já que esta
métrica, quando existem dois grafos de programas, sendo que um deles tem um controlo de fluxo
sequencial e o outro tem um controlo de fluxo encadeado, calcula o mesmo valor de complexidade
para os dois e o segundo caso deveria trazer um maior valor de complexidade.
Como se pode ver pelos estudos em cima, apesar da complexidade ciclomática de McCabe ser
bastante aceite na indústria de software, ainda existem vários estudos que lhe apontam falhas.
19
3. TESTES DE SOFTWARE
Neste capítulo será feita uma introdução aos testes de software, mostrando algumas abordagens de
testes e fazendo a sua ligação com a complexidade ciclomática.
3.1 Introdução aos testes de software
Os testes de software podem ser definidos como o resultado da execução de um software e
comparação entre o comportamento observado e o comportamento esperado, sendo o seu principal
objetivo a detecção de erros (Myers, 1989). A medição objetiva da qualidade de teste é uma das
questões-chave nos testes de software (Zhu et al., 1997), já que um importante problema na gestão
dos testes de software é assegurar que antes de qualquer teste, os objetivos desses testes são
conhecidos e aceites, e que esses objetivos são definidos em termos que possam ser medidos. Esses
objetivos devem ser quantificáveis, razoáveis e atingíveis (Ould & Unwin, 1986).
Os testes podem ser considerados como uma das principais técnicas a contribuir para uma
maior fiabilidade e qualidade do software (Del Grosso et al,. 2008), mas tal como as outras fases do
desenvolvimento de software, são atividades bastante exigentes em termos de recursos humanos,
fazendo com que seja necessário e quase obrigatório um processo continuo de procura de técnicas
com o objetivo de tornar o processo de testes o mais eficiente possível. Embora a procura dessas
técnicas consuma grande parte do tempo da investigação dos testes, é também importante fazer
software que possa efetivamente ser testado, visto que quando não são detetados erros na execução
dos testes, isto pode significar duas coisas, ou que a qualidade do software é alta ou que o processo de
testes é de baixa qualidade (McCabe et al., 1996).
No que toca aos processos de teste de software, existem várias abordagens no que toca à
tentativa de controlar a qualidade do software a ser testado. De seguida falarei de duas das mais
usadas, os testes de caixa branca e os testes de caixa preta.
3.2 Testes de caixa branca e testes de caixa preta
Na procura do erro são desenhados casos de teste na tentativa de cobrir o máximo possível de
funcionalidades de um programa. Um caso de teste é considerado bom quando tem uma alta
probabilidade de mostrar um erro não descoberto até ao momento. Erro esse que pode ser
categorizado como erro fatal, que bloqueia por completo o funcionamento do programa, ou ser apenas
20
uma falha subtil, que origina uma alteração do output, sem causar a falha do programa (Kanewala &
Bieman 2014). Se o teste não encontra erros, serve para demonstrar o âmbito no qual o programa
funciona e portanto serve para validar que o programa cumpre os requisitos.
Na literatura de testes de software, é normal encontrar os conceitos de testes de caixa branca
e testes de caixa preta. Estes são os testes mais usados na SISCOG.
Os testes de caixa preta pressupõem que o programa que está a ser testado seja uma “caixa
preta”, ou seja, é assumido que não há conhecimento sobre a forma como o programa está
implementado (Zhu et al., 1997). Este tipo de teste é tipicamente utilizado para isolar possíveis
comportamentos erróneos do sistema, tais como resultados, interfaces e rendimento. Uma das
abordagens usadas no desenvolvimento de testes de caixa preta é o teste baseado na especificação.
Esta abordagem utiliza os requisitos como forma de testar as funcionalidades, já que a especificação
dos requisitos é convertida em casos de teste, sendo que cada requisito resulta em pelo menos um
caso de teste. Segundo McCabe et al. (1996), apesar desta abordagem ser bastante usada, ela não
pode ser considerada uma solução completa, pois para além dos documentos de requisitos serem
propensos a erros, o código contempla muitos mais detalhes do que os requisitos, pois este são
normalmente escritos de uma forma muito mais abstrata. Devido a este motivo, foi argumentado que
um caso de teste que seja desenvolvido a partir de um requisito pode originar várias falhas no que toca
à cobertura total do programa.
Por seu lado, nos testes de caixa branca, existe o acesso a detalhes sobre o programa a ser
testado, sendo que os testes são feitos de acordo com esses detalhes (Zhu et al., 1997). Estes, são
utilizados para verificar o software antes da sua integração no sistema, sendo baseados em estruturas
de controlo e correção algorítmica. Neste tipo de testes, de caixa-branca, surge então o método dos
caminhos básicos criado por McCabe (1976). Com o uso deste tipo de testes, o erros são mais
facilmente detetados mesmo quando existem falhas na especificação do software, pois sabe-se que
toda a implementação do software é tida em conta. Um dos problemas destes testes, segundo McCabe
et al. (1996), é que quando um ou mais requisitos do software não são implementados, os testes de
caixa branca podem não detetar os erros que resultam dessa omissão. Portanto, tanto os testes de
caixa branca como os de caixa preta são importantes para um processo de testes efetivo.
21
3.3 Testes e complexidade ciclomática
Sabendo que os testes na SISCOG baseiam-se essencialmente na prevenção de erros, com a
complexidade ciclomática podemos estimar o número de casos de testes necessários para cobrir todos
os caminhos possíveis de uma definição. Assim sendo, a métrica da complexidade ciclomática não
pode ser só vista como um meio para quantificar a complexidade de software, já que também pode e
tem vindo a ser utilizada no apoio ao processo de testes de software (McCabe et al., 1996; Zhu, Hall, &
May, 1997; Gold, 2013).
O valor da complexidade ciclomática indica o número de caminhos possíveis de execução de
um programa, sendo este conjunto de caminhos conhecido como caminhos básicos. A sua lógica
consiste essencialmente em gerar os casos de teste de forma a que estes passem por um número
mínimo de caminhos entre a entrada e a saída do programa, sem o risco de ocorrerem redundâncias.
Baseados no número de caminhos encontrados, os casos de testes são gerados manualmente ou
através de um processo automático, de modo a cobrir todos os caminhos executáveis, o que vai
assegurar que é atingida a máxima cobertura do código (Suresh et al., 2012).
De uma forma simples, pode-se explicar a forma de cálculo do número de casos de teste
através da complexidade ciclomática da seguinte forma:
• Desenhar o grafo de controlo de fluxo;
• Determinar o número de caminhos independentes (complexidade ciclomática);
• Identificar esses caminhos;
• Para cada caminho, gerar os casos de teste (manualmente ou com uma ferramenta);
• Por fim,
o Executar cada caso de teste;
o Comparar os resultados obtidos com os resultados esperados.
Para assegurar então a máxima cobertura, todos os caminhos do programa devem ser
testados. Isto implica que, um programa com um número de complexidade alto, vai necessitar de um
maior esforço ao nível dos testes, já que quanto maior o número de complexidade, maior o número de
caminhos existentes no código.
Ao identificarmos o número de casos de testes e as zonas do código onde o esforço dos testes
deve ser concentrado, além da poupança de tempo e dinheiro, talvez se consiga ter um sistema mais
fiável (Shuman, 1990), já que os testes se podem centrar em áreas que são mais suscetíveis a erros.
23
4. GRAFOS DE CONTROLO DE FLUXOS
Neste capítulo é feita uma breve introdução à teoria de grafos e aos grafos de controlo de fluxo,
expondo alguns conceitos essenciais para posteriormente facilitar a compreensão da ligação entre a
complexidade ciclomática e os grafos.
4.1 Introdução à teoria de grafos e grafos de controlo de fluxo
A teoria de grafos é o estudo dos grafos, que são considerados como estruturas matemáticas usadas
para modelar pares de relações entre objetos. Um grafo é um conjunto de nodos ligados por caminhos,
podendo ser direcionado, em que o caminho indica a direção de ligação dos nodos, ou não-
direcionado, significando que não há qualquer distinção entre os dois nodos associados pelo caminho
(Berge, 1973).
Usando a notação de grafo pode-se usar grafos de controlo de fluxo para descrever todos os
caminhos que podem ser executados por um programa. Estes grafos têm sido objeto de vários estudos
ao longo dos anos (Jalote, 2005; Kosaraju, 1973; McCabe, 1976; Paige, 1977; Rapps and Weyuker,
1982; Tan, 2006; White, 1981; Zhu et al., 1997) e são largamente usados na análise de software
(Fenton, Whitty, & Kaposi, 1985; Kosaraju, 1973; McCabe, 1976; Paige, 1975).
São também conhecidos por grafos de programas que representam o controlo de fluxo do
programa, isto é, são grafos que descrevem a estrutura lógica de módulos software (McCabe et al.,
1996; Zhu et al., 1997). Esses módulos de software correspondem a uma única função ou sub-rotina,
que têm um único ponto de entrada e outro de saída, e podem ser usados como componente de
desenho através de um mecanismo de chamada/retorno (McCabe et al., 1996).
Os grafos de controlo de fluxo, consistem num conjunto de nodos e caminhos, sendo que os
nodos representam as instruções do programa e os caminhos representam o controlo de fluxo entre
essas instruções (Gold, 2010).
De seguida abordarei alguns conceitos fundamentais que são necessários para o estudo da
complexidade ciclomática.
24
4.1.1 Conceitos da teoria de grafos
Para o estudo elaborado neste projeto são necessários alguns conceitos da teoria de grafos. Estes
conceitos foram retirados de Berge (2001), Cardoso (2009) e Abran (2010):
Grafo
“Um grafo G, é um par (V (G), A(G)), onde V (G) é um conjunto não vazio de elementos
chamados vértices, e A(G) é uma família finíta de pares não ordenados de pares de elementos,
não necessariamente distintos, chamados arestas.”
Grafo simples
“Um grafo simples G, é um par (V (G), A(G)), onde V (G) é um conjunto não vazio de elementos
chamados vértices, nodos ou pontos, e A(G) é um conjunto finito de pares não ordenados de
elementos distintos de V (G), chamados arestas ou linhas; V (G) é o conjunto dos vértices e
A(G) é o conjunto das arestas.”
Grafo direcionado
“Um grafo direcionado (ou digrafo) D, é um par (V (D), A(D)), onde V (D) é um conjunto não
vazio de elementos chamados vértices, e A(D) é uma família finita de pares ordenados de
elementos de V (D), chamados arcos. Se D não tem laços e se os arcos de D são todos
distintos, então D é um grafo direcionado simples.”
Caminho
“Um caminho de comprimento l em G, de vi a vj, é a sequência finita de vértices em G,
vi = u0, u1, . . . , ul = vj, tais que ut−1 e ut são adjacentes para 1 ≤ t ≤ l, sendo que os vértices
são distintos (excepto, possivelmente, u0 = ul).”
Grafo ligado
“Um grafo G é ligado se, para todo o x e para todo o y (vértices), existe um percurso que liga x
e y. Um grafo que não é ligado pode ser dividido em componentes ligados.”
Grafo cíclico
“Um grafo G, em que o seu caminho que começa e acaba no mesmo vértice.”
25
Ciclo simples
“Um ciclo que tem o comprimento mínimo de 3 e no qual apenas o vértice inicial e final
podem aparecer repetidos, todos os outros aparecem apenas uma vez.”
Grafo fortemente ligado
“É um grafo direcionado que tem um caminho de cada vértice para todos os outros vértices.”
Número ciclomático
“É o menor número de arestas que devem ser removidas de um grafo para que o mesmo não
apresente ciclos.”
Tendo então presentes estes conceitos, mostrarei de seguida como McCabe se apoiou na teoria
de grafos e no número ciclomático para realizar o seu estudo da complexidade ciclomática.
4.2 Complexidade ciclomática e os grafos
A medida de McCabe é baseada na teoria de grafos (McCabe, 1976; McCabe et al., 1996), pois o seu
trabalho apoiou-se em alguns conceitos de medida presentes na teoria de grafos e na transposição
desses conceitos para o domínio da medida de software (Abran, 2010).
Segundo McCabe (1976), a abordagem seguida no estudo da medida da complexidade
centrou-se na medição e controlo do número de caminhos existentes num programa. Um dos
problemas imediatamente detetado, foi durante a medição do número total de caminhos possíveis, no
caso de existir um caminho no sentido inverso, isto é, para trás, estavamos perante a possibilidade de
obter um número infinito de caminhos. Por este motivo, o uso do número total de caminhos foi
descartado, pois não era considerado uma abordagem realista. Então, a abordagem escolhida para a
definição da complexidade ciclomática passou a focar-se no número de caminhos básicos através de
um programa, que quando combinados geram todos os caminhos possíveis.
Por outras palavras, a complexidade ciclomática expõe que para qualquer grafo de fluxo existe
um conjunto de caminhos de execução, de tal modo que cada caminho de execução pode ser expresso
como uma combinação linear dos mesmos. Um conjunto de caminhos é considerado independente se
nenhum deles for uma combinação linear dos outros. De acordo com McCabe (1976), um caminho
deve ser testado, se ele for independente dos caminhos que foram testados. Por outro lado, se um
26
caminho é uma combinação linear dos caminhos testados, ele pode ser considerado redundante (Zhu
et al., 1997).
De acordo com a teoria de grafos e segundo Berge (1973), o número ciclomático é definido
como o número de ciclos fundamentais (ou básicos) num grafo ligado e não-direcionado ao que
McCabe et al. (1996) acrescentou que era também o número de caminhos independentes através de
um grafo direcionado fortemente ligado, podendo ser calculado por:
v(G)= E - N + P
onde:
E, é o número de caminhos;
N, é o número de nodos;
P, é o número de componentes separados.
Este foi o princípio base usado por McCabe no seu estudo (McCabe, 1976), no qual este fez a
transposição desta definição para o domínio da medida de software.
Segundo Abran (2010), na engenharia de software, o programa é modelado como um grafo de
controlo de fluxo, que é uma estrutura abstrata usada pelos compiladores, em que os nodos
representam os blocos básicos, e os caminhos direcionados representam os saltos de controlo de
fluxo.
Sabendo que os grafos de controlo de fluxo de programas não são fortemente ligados, McCabe
percebeu que os poderia tornar fortemente ligados adicionando um caminho virtual que liga-se o nodo
de saída com o nodo de entrada (McCabe et al., 1996). Assim sendo, transformando o grafo de
controlo de fluxo num grafo fortemente ligado, o número ciclomático do grafo pode ser aplicado na
representação de programas (Abran, 2010). Nesta transposição, o número ciclomático, quando
aplicado ao software calcula-se da seguinte maneira:
v(G) = E - N + P + 1
Mas sabendo que esta transposição de McCabe só se aplica a módulos individuais (McCabe,
1976), o número de componentes ligados, P, é sempre igual a 1, ficando então definida a fórmula de
cálculo da complexidade ciclomática por:
v(G) = E - N + 2
27
McCabe (1976), sugeriu que, a fórmula, v(G) = E – N + 2, por associação, é a medida da
complexidade de um programa, à qual ele chamou de complexidade ciclomática e interpretou como a
quantidade de decisão lógica num módulo único de software (McCabe et al., 1996).
Por fim, no seu trabalho original, McCabe (1976) listou algumas propriedades da complexidade
ciclomática:
• v(G) ≥ l.
• v(G) é o número máximo de caminhos linearmente independentes em G; é o tamanhos de um
conjunto básico.
• Inserir ou apagar instruções funcionais em G não afeta v(G).
• G tem apenas um caminho se, e apenas se, v(G) = 1.
• A inserção de um novo caminho em G, aumenta o v(G) uma unidade.
• v(G) depende apenas da estrutura de decisão de G.
29
5. IMPLEMENTAÇÃO
Neste capítulo será explicado todo o processo de desenvolvimento, mostrando qual a linguagem de
programação usada, quais as ferramentas que serviram de suporte e explicando as decisões tomadas
durante a implementação.
5.1 Abordagens ponderadas
Na discussão sobre que caminho seguir para a elaboração deste projeto, foram discutidas duas
alternativas para a implementação da ferramenta.
A primeira passava pela criação de um parser, que é uma metodologia muito utilizada em
ferramentas do género, tendo esta abordagem sido posteriormente abandonada muito por causa das
particularidades do LISP. Entre algumas dessas particularidades que levaram a esta decisão, surgiu o
fato de esta linguagem permitir a criação macros que, em LISP, podem ser vistas como extensões da
linguagem e que podem, entre outras utilizações, permitir a definição de novos condicionais,
iteradores, etc. Isto faria com que fosse impossível cobrir todos estes casos com a utilização de
parsers.
A segunda, e que acabou por ser a escolhida, passava pela utilização de um code walker. Um
code walker permite que todas as macros sejam totalmente expandidas, isto é, sejam transformadas
em instruções básicas e nativas, removendo assim a limitação encontrada na primeira abordagem que
tinha sido ponderada. Esta ferramenta retorna como resultado uma Abstract Syntax Tree, que é uma
representação abstrata e simplificada, em forma de árvore, da estrutura semântica do código. Esta
Abstract Syntax Tree, foi então o ponto de partida para a análise proposta para este projeto, que passa
pela criação do grafo e cálculo da sua complexidade ciclomática.
5.2 Linguagem de programação e ferramentas externas de apoio
Aqui serão demonstradas todas as ferramentas utilizadas no desenvolvimento do projeto, sendo que a
tabela seguinte serve como uma pequena introdução ao que vai ser falado nesta secção.
30
Linguagem/Ferramentas Breve descrição
LISP Linguagem de programação
GNU Emacs + SLIME Editor de texto com o modo para
desenvolvimento em Common LISP.
Allegro CL IDE IDE; Ambiente de desenvolvimento integrado
para o Common LISP.
Quicklisp Gestor de bibliotecas LISP.
CL-Graph Biblioteca de grafos.
Graphviz Software para visualização de grafos.
hu.dwim.walker Code walker.
Tabela 3 - Ferramentas utilizadas no projeto.
5.2.1 LISP
Começando pela linguagem de programação usada, o LISP (McCarthy, 1960), abreviatura para List
Processing, é uma linguagem criada por John McCarthy, no final dos anos 50 e que, como podemos
ver pelo nome, tem como ideia principal usar listas como a estrutura para os dados e para o código.
Esta, é a linguagem de programação mais utilizada na SISCOG, sendo uma linguagem que
está fortemente ligada ao mundo da inteligência artificial. Existem diversos dialetos LISP, sendo que o
usado neste projeto foi o Allegro Common LISP.
Figura 7 - Linguagens de programação usadas na SISCOG. (SISCOG, 2013).
31
5.2.2 GNU Emacs e SLIME
O GNU Emacs, criado por Richard Stallman, é um dos editores de texto mais conhecido, enquanto que
o SLIME, criado por Luke Gorrie e Helmut Eller, é um modo do Emacs para o desenvolvimento em
Common LISP, que permite dotar o Emacs de um conjunto de funcionalidades adicionais que
melhoram a interacção entre o Emacs e o LISP.
Na SISCOG o Emacs é muito utilizado pois, para além de ser usado como editor de texto para
os desenvolvimentos em LISP, é também através dele que os analistas-programadores lançam as
aplicações da empresa em modo de desenvolvimento, criando uma ligação com o IDE Allegro CL que
será abordado de seguida.
5.2.3 Allegro CL IDE
O Allegro CL é um ambiente de desenvolvimento criado pela Franz Inc. e que corresponde à
implementação do LISP que é usada na SISCOG para desenvolver, compilar e distribuir as aplicações
da empresa. Algumas das vantagens apontadas (SISCOG, 2013) pelos analistas-programadores são:
• IDE com trace dialog, profiler, class browser;
• Compilador rápido;
• Multiprocessamento simétrico (SMP).
5.2.4 Analisador de código: code walker
O code walker hu.dwim.walker, foi umas das ferramentas mais importantes para o desenvolvimento do
projeto, já que através dele foi possível aceder às Abstract Syntax Tree que contêm toda a informação
necessária para a construção do grafo.
Para que se perceba melhor as vantagens de se utilizar um code walker para LISP é necessário
compreendermos quais os principais passos executados pelo compilador:
1) O texto é convertido em s-expressions (árvores de células cons, com símbolos, números, etc),
sendo que isso é feito pelo reader do LISP.
2) As macros são expandidas.
3) O resultado é compilado.
32
O code walker baseado no resultado da fase 2, fornece uma Abstract Syntax Tree com o
resultado de todas as macros totalmente expandidas. Isto foi também um dos pontos chaves para a
utilização do code walker neste projeto, pois era importante ter as macros totalmente expandidas, já
que em LISP as macros permitem extender a sintaxe da linguagem o que poderia criar problemas na
análise do código.
Um exemplo da utilização de macros no LISP pode ser observado pelo facto de existirem na
própria especificação da linguagem algumas macros para representar algumas instruções nativas,
como por exemplo a instrução cond. Essa instrução pode ser representada na sua forma mais primitiva
como um conjunto de instruções if encadeadas:
Figura 8 - Exemplo de um if e um cond em LISP.
Se se utilizar o macroexpand do LISP para ver a expansão da macro cond da Figura 8:
Figura 9 - Exemplo de um macroexpand.
Obtem-se o seguinte resultado:
Figura 10 - Resultado do macroexpand.
Como se pode observar pela Figura 10, o cond não é totalmente expandido para as suas
formas mais primitivas, pois, nem todas as suas as condições foram retornadas como if.
Sabendo que a instrução cond faz nativamente parte da especificação do LISP e por esse
motivo, se a abordagem de utilização de um parser fosse seguida, esta instrução iria certamente ser
considerada, não seria possível precaver todas as outras possibilidades que um programador de LISP
tem ao seu alcance quando lhe é facultada esta facilidade de extender a linguagem. Na SISCOG, e
tirando partido desta vantagem de extensão da linguagem, existem muitos iteradores criados
33
internamente e que fazem parte da API dos produtos da empresa, sendo que não seria viável conseguir
uma cobertura total de todos estes casos através da utilização do parser.
Com a utilização do code walker, este problema não existe pois todas as macros são
totalmente expandidas, ou seja, todas elas estão nas suas formas primitivas, o que tomando os casos
dos condicionais como exemplo, faria com que todos eles, na Abstract Syntax Tree do resultado,
tivessem a mesma forma, ou seja, uma if-form. Na tabela seguinte podemos ver algumas das formas
retornadas pelo code walker, assim como alguns exemplos do que estas abrangem:
Formas: O que abrange:
If-form if; cond; when; unless.
Block-form dolist; do; do*.
Free-application-form Chamada a definições não nativas, dentro da
definição analisada.
Progn-form Instrução agrupadas propositadamente.
Constant-form Variáveis constantes.
Let-form let; let*, labels; flet; multiple-value-bind.
Setq-form setf; setq.
Tabela 4 - Formas retornadas pelo code walker.
5.2.5 Gestor de bibliotecas LISP: Quicklisp
O Quicklisp é um gestor de bibliotecas para Common Lisp, que permite descarregar, instalar e carregar
bibliotecas usando apenas comandos simples. Este gestor foi utilizado para descarregar a biblioteca
hu.dwim.walker, que foi o code walker necessário para este projeto.
Para instalar o quicklisp usa-se o seguinte comando:
(quicklisp-quickstart:install)
E para descarregar/carregar a biblioteca do code walker:
(ql:quickload "hu.dwim.walker ")
34
5.2.6 Biblioteca de grafos: CL-Graph
O CL-Graph é uma biblioteca de Common LISP usada para a manipulação de grafos, e que contém,
entres outros, uma série de algoritmos orientados a grafos.
No âmbito deste projeto, a utilização desta biblioteca foi feita de modo muito simplista, de acordo
com as necessidades que foram identificadas, tendo sido usadas as seguintes definições desta
biblioteca:
• add.vertex, para adicionar um vértice ao grafo;
• delete.vertex, para apagar um vértice do grafo;
• iterate.vertexes, para percorrer os vértices existentes no grafo;
• add.edge, para adicionar um caminho entre dois vértices;
• delete.edge, para apagar um caminho entre dois vértices.
5.2.7 Graphviz
O Graphviz, abreviatura para Graph Visualization Software, é um software de código livre usado para o
desenho de grafos que sejam especificados, entre outros, em scripts de linguagens DOT. Esta
linguagem é usada para descrever grafos através de um formato simples de texto, sendo que esse
texto é posteriormente usado pelo Graphviz para a criação de um ficheiro de imagem com o grafo
correspondente.
Figura 11 - Exemplo de um grafo em linguagem DOT.
Neste projeto, esta ferramenta foi utilizada durante o processo de construção do relatório de
complexidade e toda a implementação foi pensada para que esta operação fosse realizada de forma
completamente automática, não sendo para isso necessária qualquer intervenção do programador.
Mais pormenores sobre esta implementação são dados no Capítulo 5.5 - Relatório.
35
5.3 Estrutura do código
A estrutura do código foi pensada da seguinte forma:
Figura 12 - Estrutura geral do código implementado.
Este processo tem início com a construção do grafo. Numa primeira fase é feita uma verificação
à definição que está a ser analisada, com o intuito de identificar se esta definição tem ou não variáveis
e, em caso afirmativo, é chamada outra definição para lidar com essa variáveis, criando os nodos
respetivos e ligando-os entre si, devolvendo como resultado a parte inicial do grafo com esta
informação. Este passo é importante, porque em LISP podem existir pontos de decisão na definição
das variáveis, o que faz com que a própria definição de variáveis possa influenciar a complexidade
ciclomática de um programa.
Tendo a parte introdutória do grafo definida, começa-se a avaliar o corpo da definição, que
corresponde a um ciclo onde se avaliam todas as instruções do corpo, transformando cada instrução
num nodo do grafo e criando os respetivos caminhos entre instruções, representando assim a ordem
de execução das mesmas. Quando este processo acaba, é calculada a complexidade ciclomática,
36
usando o número de nodos e caminhos do grafo calculado anteriormente. Findo este processo é
gerado o relatório com todas estas informações.
De seguida irão ser abordadas com mais detalhe as decisões tomadas durante todo o processo
de implementação.
5.4 Decisões de implementação
Depois de definida a forma sobre como o projeto se devia desenrolar e qual a estrutura do código,
começou-se então com a construção da ferramenta com base nos casos mais simples possíveis e que
serão descritos de seguida. Esses casos mais simples foram dando origem a casos cada vez mais
complexos, podendo a ferramenta, no fim da implementação, lidar com definições de diferentes
complexidades, isto é, desde as mais simples às mais complexas.
Os casos analisados de seguida encontram-se agrupados como mostra a seguinte tabela:
Grupo Principais palavras reservadas
Condicionais if; cond; when; unless.
Iteradores dolist; do; do*.
Operadores lógicos and; or.
Operadores especiais let; let*; labels; flet; multiple-value-bind.
Outros setf; list; cons; push; format; print.
Tabela 5 - Grupos de palavras reservadas.
5.4.1 Definição genérica
Foi criada uma definição genérica, para controlar o tipo da forma devolvida pelo code walker,
identificando assim qual a definição que deve ser usada para determinada forma, ou seja, de cada vez
que se vai construir um nodo do grafo, esta definição é chamada para verificar qual o seu tipo de
forma, fazendo posteriormente a chamada da definição responsável por tratar esse tipo.
5.4.2 Operador lógico: and e or
Os operadores lógicos and e or permitem agrupar condições, podendo ser usados de forma direta, na
definição de variáveis, ou em condições de ciclos. Neste sub-capítulo é abordada a utilização destes
37
operadores de forma direta, sendo que a sua utilização nas instruções condicionais irá ser abordada
mais à frente no Capítulo 5.4.6 - Condicionais compostos.
A representação num grafo destes operadores é igual em ambos os casos, mudando apenas
os caminhos no caso das suas condições serem verdadeiras ou falsas, ou seja, num and se a condição
é verdadeira avança-se para a condição seguinte, enquanto que no or se avança para o fim; no caso da
condição ser falsa, no and avança-se para o fim e no or para a condição seguinte. Esta troca de
caminhos em termos de valor final de complexidade, não tem qualquer relevância mas, faz com que os
dois operadores sejam interpretados de maneira distinta pelo code walker, o que devido à
complexidade de manutenção do código que isso poderia acarretar devido à validação dos vários
caminhos existentes, fez com que se optasse pela criação de duas definições distintas para lidar com
os operadores and e or.
Figura 13 - Grafo de um and/or.
Na implementação destas definições, foi necessária a introdução de um nodo fictício pois,
como podemos observar pela Figura 13, na última condição, “condicao2”, se não fosse introduzido um
nodo fictício, o caminho entre o nodo dessa condição e o nodo final, tanto no caso de ser verdadeiro
como no caso de ser falso seria o mesmo, o que introduziria um erro no cálculo da complexidade, pois
a biblioteca de grafos não permite duas ligações do tipo A-B.
Foi também contemplada a possibilidade destes operadores terem nodos intermédios, como
mostra a seguinte figura:
38
Figura 14 - Grafo de um and/or com nodos intermédios.
Para lidar com esta situação da existência de nodos intermédios, foram criadas mais duas
definições, uma para cada operador, sendo estas responsáveis pela construção dos nodos do grafo e
dos seus caminhos. Devido à possibilidade de existência de mais do que um nodo intermédio, estas
definições foram implementadas usando recursividade, ou seja, estas definições são chamadas
recursivamente enquanto existirem nodos.
Quando o processo dos nodos intermédios acaba, este sub-grafo é ligado ao grafo produzido
pelas definições principais do and ou or, ficando apenas a existir um grafo completo.
5.4.3 Condicional simples: if
O primeiro condicional a ser implementado foi o if, pois é a forma básica de todos os condicionais
existentes no LISP, sendo que o seu grafo, no caso mais simples pode ser definido como mostra a
seguinte figura:
39
Figura 15 - Grafo de um if simples.
Este é o caso mais simples do if, onde existe apenas uma condição e uma instrução em cada
ramo. Neste caso específico, tendo apenas uma condição e uma instrução em cada ramo, o grafo
construído contempla estes três nodos, assim como os nodos “início” e “fim”.
Na implementação foi também considerado o caso de existirem várias condições no if, com o
recurso aos operadores lógicos and e or. Estes casos especiais, assim como os restantes condicionais
que vão ser abordados de seguida, resultaram numa única definição. Esta definição lida com todos os
condicionais e, por isso mesmo, é a mais complexa de todo o projeto, necessitando ainda de um
trabalho de modularização. Todas as decisões tomadas relativas a esta implementação e o processo de
construção do grafo serão explicadas mais à frente no Capítulo 5.4.6 - Condicionais compostos.
Convém destacar que, em LISP, o then e o else do if apenas podem conter uma instrução
cada, caso contrário torna-se necessário o uso de outros operadores especiais como o progn ou o let
para agrupar instruções, ou então, em vez do if usar a macro cond.
5.4.4 Condicional simples: cond
A macro cond é bastante utilizada em LISP e pode ser definida como um conjunto de if’s encadeados.
Esta, ao contrário do if, permite definir várias instruções para a mesma condição sem ser necessário
recorrer a operadores especiais.
Na figura seguinte pode-se visualizar o grafo resultante da utilização da macro cond com uma
ou várias condições.
40
Figura 16 - Grafos da macro cond.
O cond vai verificando as suas condições sequencialmente, sendo que quando uma se verifica,
as suas intruções são executadas e mais nenhuma condição é verificada. No caso de nenhuma das
condições se verificar, são executadas as instruções da condição representada no grafo da Figura 16
pelo nodo otherwise.
Como a macro cond, é analisada pela mesma definição do if, os pormenores sobre a mesma
serão abordados no Capítulo 5.4.6 - Condicionais compostos.
5.4.5 Condicional simples: when e unless
O when e o unless pertecem também aos condicionais do LISP. O grafo destes, da mesma forma que
o do and e do or, é igual entre si, ou seja, o grafo produzido para ambas as situações só difere nos
caminhos de verdadeiro ou falso, o que não tem qualquer relevância para o cálculo da complexidade.
Mais uma vez, o code walker, interpreta-os de maneira diferente, pelo que na definição dos
condicionais exista uma verificação para cada um destes, de forma a ser possível fazer uma validação
correta dos caminhos entre nodos. O grafo destes condicionais pode ser consultado de seguida:
41
Figura 17 - Grafo de um when/unless.
5.4.6 Condicionais compostos
A complexidade dos condicionais pode crescer consideravelmente com a introdução dos operadores
lógicos and e or na definição das condições pois, com o recurso a estes operadores podem ser
definidas várias condições nos ciclos, o que faz com que a complexidade ciclomática seja maior, pois
vão existir mais pontos de decisão no código.
Usando um pequeno exemplo em pseudo-código de um if:
Figura 18 - Pseudo-código de um if com recurso aos operadores and/or.
42
Isto resultaria nos seguintes grafos:
Figura 19 - Grafos de um if com recurso aos operadores and/or.
O processo de implementação da definição dos condicionais, baseou-se na informação
fornecida pelo code walker, que no caso dos condicionais é uma Abstract Syntax Tree com a condição,
o ramo do else e o ramo do then. Qualquer um destes (condição e ramos), no caso mais simples,
resulta em três nodos, mas no caso dos compostos, qualquer um desses nodos pode gerar um novo
grafo. Então o primeiro passo da definição é a identificação do tipo de condição, que pode ser um dos
seguintes:
• condição com recurso a um and;
• condição com recurso a um or;
• condição simples.
Se o resultado desta verificação for uma condição simples, é criado um nodo para essa
condição e o processo de construção do grafo continua para a análise dos ramos. Caso contrário,
consoante o resultado da verificação é chamada a definição do and ou do or e, se necessário, são
também usadas as definições que lidam com os nodos intermédios destes operadores lógicos. Foi
também considerado na implementação destes casos, o caso de existirem condições que contenham
tanto o and como o or na mesma condição, sendo para isso feitas validações extra aos caminhos.
No fim deste processo, tendo o grafo das condições completo, são analisados os ramos then e
else. Através da definição genérica, começa-se mais uma vez por identificar qual o seu tipo, o que tanto
pode resultar num único nodo, no caso de uma instrução simples, como num conjunto de nodos, no
43
caso de a instrução ser um condicional, um iterador ou um operador especial. Durante a análise
destes, são criados os nodos das suas instruções e seus respetivos caminhos. No fim deste processo
de análise, faz-se a ligação entre os nodos da condição e os nodos dos ramos, obtendo-se então o
grafo completo.
Uma dificuldade encontrada foi o comportamento do code walker na análise dos diferentes
condicionais, já que este, apesar de muitos casos resultarem num grafo idêntico, os interpreta de
maneira diferente, o que fez com que fosse necessário validar todos os caminhos possíveis entre os
objetos em estudo.
De um modo simplista, o fluxo da definição que trata dos condicionais pode ser definida da seguinte
maneira:
• Verificar o tipo dos nodos (condição, then, else);
• Chamar a definição genérica para cada um desses nodos, direcionando assim a execução do
código para as definições necessárias (outros condicionais, iteradores, constantes, etc);
• Tendo os três nodos (condição, then, else), que podem ser grafos, ligá-los entre si, validando
todos os caminhos. Esta foi a parte mais complexa deste projeto, pois existia uma imensidão
de caminhos para validar, o que levou a que fosse necessária a introdução de várias
condições para contemplar todos os casos estudados.
5.4.7 Iterador: dolist
No estudo dos iteradores decidiu-se começar pelo dolist, já que é um dos iteradores de listas mais
usados em LISP.
No seu processo de implementação foi necessário contemplar dois casos, um deles onde o
iterador contém várias instruções no seu corpo e outro, onde só tem uma instrução. Esta separação foi
necessária, pois no caso de só ter uma instrução no corpo, sendo essa instrução uma constante, é
necessária a criação de um nodo fictício, pois a biblioteca de grafos usada não permite ligações do tipo
A-B B-A. O seu resultado pode ser visto nos seguintes grafos:
44
Figura 20 - Grafos do dolist.
Foi também definido que o fluxo do processo de criação do grafo do dolist devia seguir a
ordem de execução do mesmo, iniciando-se com a criação do nodo da condição e depois partindo para
a análise do seu corpo.
Na análise do corpo foi então necessário verificar o tamanho do mesmo. Se este valor for 1,
antes de ser criado o nodo fictício é verificado se a instrução é uma constante, o que em caso
afirmativo resulta num único nodo, o da instrução, e na criação de um nodo fictício como mostra a
figura. Esta verificação do tipo de instrução é necessária, porque se a instrução não for uma constante,
este nodo vai ter que ser expandido com o resultado da análise da instrução, fazendo com que o nodo
seja na verdade um grafo. Neste caso, já não é precisa a criação do nodo fictício, porque
transformando-se a instrução do corpo em vários nodos, ou seja, num sub-grafo, o último nodo desse
sub-grafo ligar-se-á à condição do dolist, não havendo então o problema de se ter uma ligação A-B B-A,
o que posteriormente resultaria num erro na ferramenta devido à limitação da biblioteca de grafos.
No caso do tamanho ser maior que 1, é feito o mesmo processo descrito em cima para
quando o tamanho é 1 e a instrução não é uma constante, isto é, é chamada a definição genérica para
verificar o tipo da instrução e posteriormente chamar a definição adequada para analisar o tipo em
causa.
Por fim, o último nodo resultante dessa análise é ligado ao nodo da condição do dolist,
resultando outra vez num único grafo.
45
5.4.8 Iterador: do/do*
Os iteradores do e do* foram os outros iteradores estudados. Foi necessário diferenciá-los do dolist,
pois apesar de o code walker os ler como sendo do mesmo tipo, estes funcionam de maneira diferente,
permitindo definir variáveis e os seus incrementos antes da condição, sendo as variáveis usadas no
corpo do iterador e sendo os incrementos feitos após a execução do corpo e antes de se voltar a
analisar a condição.
Uma dificuldade encontrada foi a análise destes dois iteradores por parte do code walker, pois
este lê as variáveis do do e do do* de maneira diferente. Para resolver este problema foi necessário
acrescentar uma verificação para distinguir qual das situações está a ser analisada através de
informação encontrada na Abstract Syntax Tree. O do e do* partilham o mesmo grafo, que pode ser
consultado de seguida:
Figura 21 - Grafo de um do/do*.
O nível de complexidade destes iteradores pode aumentar consideravelmente, quando se
definem várias condições de paragem, como se pode ver pelo grafo que se segue:
46
Figura 22 - Grafo 2 de um do/do*.
A complexidade pode aumentar ainda mais, pois para além do número de condições, é
possível que o nodo do corpo seja passível de expansão. Para resolver este problema foi decidido
tornar o código um pouco mais modular, sendo que cada um dos módulos é responsável por
determinada tarefa, e no seu conjunto resolvem a problemática associada a estes iteradores.
O fluxo da definição principal pode ser definido da seguinte forma:
• Construção da parte do grafo relativa às variáveis do iterador;
• Construção da parte do grafo relativa às condições;
• Ligação entre variáveis e condições;
• Construção dos seus nodos/grafos e respectivas ligações do corpo;
• Ligação entre as condições e o corpo;
• Construção parte do grafo relativa aos incrementos, steps, das variáveis;
• Ligação entre o corpo e os incrementos;
• Ligação entre os incrementos e as condições.
47
5.4.9 Operadores especiais: let e let*
Estes operadores especiais, let e let*, são usados com bastante frequência, pois permitem definir
variáveis que podem depois ser usadas no seu corpo. A diferença entres os dois operadores é apenas
na forma de execução das variáveis, não havendo diferenças no valor da complexidade usando
qualquer um deles.
Na implementação foram considerados dois casos de uso do let. O primeiro, com o let a ser
usado para definir um ambiente léxico e as suas variáveis, no qual está incluída também a criação de
definições, e o segundo, a ser usado em situações em que é necessária a criação de variáveis locais
dentro do corpo de uma determinada definição. Esta diferenciação deveu-se ao modo como o code
walker analisa estes dois casos.
No processo inicial da implementação, quando se verifica qual o tipo da definição a ser
estudada, o que se está realmente a fazer é a verificar se a definição contém ou não um let, isto é, se a
definição tem variáveis definidas, pois em caso afirmativo, é necessário criar os nodos para essas
variáveis e ligá-los entre si, para posteriormente serem ligadas com o corpo da definição. O grafo do let,
como podemos ver de seguida, não apresenta grande complexidade:
Figura 23 - Grafo de um let/let*.
Neste projeto, a única diferença entre o let e o let*, é mais uma vez, a forma como o code
walker os analisa pois, apesar de na implementação representarem o mesmo, este não os lê da
mesma maneira. Para resolver este problema, foi necessário introduzir uma verificação para os
distinguir, pois caso contrário, isto originaria um grafo com os caminhos trocados.
48
Outra decisão de implementação, foi a criação de uma definição específica para lidar com a
definição de variáveis no let, pois quando estamos a lidar com a definição de variáveis em LISP, temos
que ter algum cuidado, pois é possível introduzir complexidade nas mesmas através de condicionais ou
iteradores, causando isto um aumento do valor de complexidade ciclomática. Nesta definição é então
necessário verificar sempre o tipo do que está a ser atribuído à variável, pois no caso de não ser uma
constante, em vez de existir um único nodo, passa-se à expanção desse nodo, tornando-o num
pequeno grafo que é posteriormente ligado ao resto das variáveis.
O processo de análise de definições que contenham o let, começou então a ser definido pela
chamada da definição que analisa o tipo das variáveis e constrói o seu grafo. Isto faz com que o grafo
das variáveis vá sendo criado à medida que todas as instruções vão sendo analisadas. Quando este
processo acaba e todas as variáveis estão analisadas, o grafo destas é ligado ao primeiro nodo do
corpo. Na análise do corpo é, mais uma vez necessário proceder à expansão dos nodos das várias
instruções, fazendo uma análise sequencial dessas mesmas instruções, garantindo assim que a
criação de nodos e as suas ligações se encontram bem feitas.
5.4.10 Operadores especiais: labels e flet
No LISP, este tipo de operadores são bastante úteis, pois permitem criar um ambiente léxico onde é
possível definir funções localmente, isto é, permitem definir funções que poderão ser usadas dentro do
contexto da definição onde estão definidas.
O code walker mostrou-se muito útil na análise de definições que contenham funções
localmente definidas através do labels ou do flet, pois durante a análise das instruções do corpo destas
definições, consegue-se aceder à Abstract Syntax Tree dessas funções locais. Isto fez com que se
decidisse criar o grafo destas funções locais apenas quando estas são chamadas em alguma instrução
da definição principal, tornando assim os caminhos do grafo mais fiáveis e completos.
Em termos de funcionamento, o labels e o flet funcionam de maneira semelhante, sendo que a
diferença entre os dois para este projeto é, à semelhança do que acontece com o let e o let*, a forma
como o code walker os analisa.
Quanto ao processo de construção do grafo destes operadores, foi então decidido começar a
análise pelas instruções do corpo do operador, analisando as funções locais apenas quando estas são
invocadas. Aqui verifica-se normalmente o tipo de instrução, cria-se o nodo da mesma e os caminhos
entre nodos devidamente validados. No fim deste processo obtém-se então o grafo final.
49
5.4.11 Operadores especiais: multiple-value-bind
O multiple-value-bind é um operador especial que cria um ambiente léxico e permite executar uma
definição, guardando em variáveis todos ou parte dos resultados desse processo, sendo depois possível
usar essas variáveis nas instruções do seu corpo.
O code walker analisa o multiple-value-bind como sendo um let, o que levou a que fosse
necessário adicionar uma verificação na definição de análise do let para detetar quando se estava na
presença de um multiple-value-bind e aí chamar a definição criada para fazer a análise do mesmo.
O fluxo da definição de análise deste operador inicia-se com a criação de um nodo para a
definição que está definida no multiple-value-bind, passando de seguida para a análise de todas as
instruções do corpo do operador, criando os nodos respetivos e expandindo-os caso necessário.
Simultaneamente são também criadas as ligações entre nodos, validando sempre todas essas ligações.
5.4.12 Outros
Existem várias definições implementadas, mas que não vão ser abrangidas aqui, pois o seu nível de
complexidade é bastante baixo e pouco de relevante há a dizer sobre elas. Estas definições são todas
aquelas em que uma instrução é transformada num único nodo, como por exemplo as constantes, o
format, o push, etc.
5.5 Relatório
Um dos objetivos deste projeto era a disponibilização de um relatório, exportado em formato HTML,
que reunisse toda a informação da análise de complexidade ciclomática efetuada. Foi definido que os
constituintes do relatório seriam:
• Logótipo da empresa;
• Nome da definição analisada;
• Valor da complexidade ciclomática;
• Grafo gerado;
• Código analisado.
Relativamente ao processo de implementação do mesmo, foi necessária a implementação de
várias definições para se conseguir que o relatório cumprisse os objetivos, sendo que toda a
50
implementação inerente a este foi codificada em LISP. As definições implementadas cobrem os
seguintes pontos:
• Recolha da informação do grafo;
• Criação das etiqueta dos nodos e caminhos; sendo isto opcional;
• Transformação dos nodos e caminhos no formato DOT;
• Transformação do grafo para o formato DOT;
• Transformação do ficheiro DOT em imagem;
• Geração do relatório em HTML.
Dos pontos referidos em cima, destaca-se a transformação do grafo resultante da análise de
complexidade num ficheiro de formato DOT e a sua consequente transformação em imagem.
Para a transformação do grafo num ficheiro foram criadas várias definições em LISP, que
tratam de todo processo de manipulação do grafo e criação e gravação do ficheiro.
Para a transformação do ficheiro em imagem, foi usada uma definição do LISP que permite
chamar automaticamente a linha de comandos, na qual foi passado o comando que corre o Graphviz e
transforma o ficheiro DOT em imagem. Esse comando foi introduzido no código de maneira a que não
seja necessário realizar qualquer operação manual por parte do programador, fazendo com que este
processo seja totalmente automático e impercetível ao utilizador.
O relatório é construído depois de todo o processo de análise de complexidade ter acabado
pois, como pode ser visto em cima, necessita de informação dessa análise para poder ser construído,
o que implica que antes do relatório ser gerado, é obrigatória a existência da imagem do grafo e o
cálculo do valor da complexidade ciclomática.
Um exemplo do relatório pode ser consultado no Anexo I – Relátorio gerado para o caso I.
51
6. ESTUDO DE CASOS
Neste capítulo serão analisados os três casos de estudo escolhidos para este projeto, dando um
pequeno enquadramento das definições no código global, falando no seu objetivo e características.
Todos os casos de estudo aqui apresentados pertencem a especializações do produto CREWS, usado
para o planeamento e gestão de pessoal, sendo essas especializações feitas para clientes concretos da
SISCOG.
6.1 Caso I – Definição de cálculo de compensações
A definição escolhida para servir como introdução do projeto tem como objetivo realizar o cálculo das
compensações dos trabalhadores da DSB S-tog, que é uma companhia dinamarquesa que opera, entre
outros, os comboios urbanos de Copenhaga.
A definição em estudo surge no meio do processo de cálculo oficial de compensações, sendo
que este processo pode ser definido da seguinte forma:
• Definição dos recursos a calcular;
• Cálculo das compensações;
• Geração de relatório;
• Gravação dos dados.
Este processo de cálculo de compensações inicia-se então com a escolha dos recursos para os
quais vão ser calculadas as compensações, sendo de seguida chamada a definição em estudo para
cada um dos recursos selecionados anteriormente. Esta definição tem como objetivo fazer o cálculo de
compensações de cada recurso num dado período de tempo. Após este cálculo estar concluído para
todos os recursos, é produzido um relatório e são gravados os dados.
52
Figura 24 – Definição do cálculo de compensações.
Da análise de complexidade ciclomática efetuada, obteve-se um valor ciclomático de 9 e o
grafo mostrado de seguida:
Figura 25 - Grafo do caso de estudo I.
53
Como referido anteriormente, a ferramenta calcula a complexidade ciclomática aplicando a
fórmula original de McCabe, usando para isso o número de caminhos e nodos, que como se pode
constatar pela Figura 25, são 25 e 18, respetivamente, ou seja:
V(G) = E – N + 2
V(G) = 25 – 18 + 2
V(G) = 9
Este valor pode ser confirmado manualmente pela fórmula alternativa, V(G) = P + 1, onde P é o
número de pontos de decisão.
Os oito pontos de decisão identificados na Figura 25 são os nodos representados por todos os
condicionais when e if, e pelos iteradores dolist e “it_datas”, que é uma macro que corresponde a um
iterador criado internamente na SISCOG para iterar datas. Cada um destes pontos de decisão realiza
uma verificação específica tal que o when1 verifica se existem compensações, o if é uma verificação
pelo tipo de compensação, os “when2_1” e “when2_2” representam um when com duas condições,
ou seja, a condição do when é constituída por um and com duas condições. O mesmo se passa no
“when3_1” e “when3_2”. O dolist e o “it_datas” são iteradores em que o dolist é utilizado para
percorrer todas as compensações do recurso (que podem ser várias) e o “it_datas” é utilizado para
percorrer um conjunto de datas.
Fora dos pontos de decisão surgem os nodos “varX” que correspondem a definições de
variáveis, os setf que são atribuições de valores e o “resultado” que é valor do cálculo das
compensações de um determinado recurso.
De acordo com a Tabela 2, um valor de complexidade ciclomática de 9, representa um baixo
risco de erro, sendo o código facilmente testável. Não há portanto muitos comentários a acrescentar.
No Anexo I – Relátorio gerado para o caso I pode ser consultado o relatório gerado para esta definição.
6.2 Caso II – Definição de expansão de partilha de equipamento
O segundo caso de estudo escolhido foi uma definição que é usada pelo sistema CREWS_NS, que é
um sistema desenvolvido para os caminhos de ferro holandeses. Esse definição é utilizada quando
estão a ser calculados quais os comboios que podem ou não partilhar equipamento.
54
Esta definição foi escolhida por ser bastante rica sintaticamente, possibilitando o teste da
ferramenta para diferentes condicionais e iteradores.
O processo onde a definição é utilizada inicia-se com a definição de intervalos de séries de
comboios, num parâmetro gráfico, onde se define quais as séries de comboios que podem ou não
partilhar equipamento. A definição é então chamada com esse intervalo definido de séries de comboio
e expande-o, isto é, torna todos os comboios dessa série como comboios possíveis de partilhar (ou
não) equipamento, gravando o resultado numa hash-table. No fim de todo o processo, o resultado é
mostrado na interface gráfica com uma marca visual sobre os comboios afetados.
Como se pode ver pelo grafo do Anexo II – Grafo do caso de estudo II, foram calculados 54 caminhos e
38 nodos, o que resulta numa complexidade ciclomática de 18.
V(G) = E – N + 2
V(G) = 54 – 38 + 2
V(G) = 18
De acordo com a Tabela 2, o valor ciclomático desta definição encontra-se num nível
intermédio (entre 11 e 20), apresentado por isso um risco moderado para a manutenção e para os
testes.
Apesar de não ser uma definição muito grande (36 linhas de código), apresenta um número
ciclomático que é metade do número de linhas de código, devido à presença de várias condições na
definição. A figura que se segue, tem uma parte do grafo que contém algumas dessas condições.
55
Figura 26 - Exemplo de parte do grafo do caso de estudo II.
Como se pode constatar pela Figura 26, neste pequeno pedaço do grafo, a definição contém
um when com oito condições, sendo que estas condições representam quase metade da complexidade
total do grafo. A condição do nodo representado na Figura 26 como “when3_X”, onde X é um número
e representa uma condição, é expressa em LISP da seguinte forma:
56
Figura 27 - Condições do when no caso de estudo II.
Este pequeno pedaço de código criou alguns desafios ao nível da implementação, no que toca
à validação de caminhos, pois a abordagem de desenho inicial da ferramente não tinha sido
suficientemente flexível para se conseguir lidar com situações desta complexidade. Os desafios que
daqui surgiram permitiram posteriormente ter uma ferramenta mais completa.
O resto das condições do grafo baseiam-se nas condições de paragem dos iteradores do e
dolist, assim como nas condições do when e unless espalhados pelo corpo da definição.
Esta é uma definição bastante rica em termos de conteúdo, tendo servido como principal forma
de apoio para a elaboração do projeto, pois nela podemos encontrar uma variedade de casos para
testar a implementação com o let, dolist, do, do*, multiple-value-bind, labels, when e unless.
6.3 Caso III – Definição de processamento de licitações
Como último caso de teste para este projeto, foi escolhida a definição que trata do processamento de
licitações dos trabalhadores. Tal como no caso I, esta definição é usada pelo sistema de gestão de
recursos da DSB S-tog.
O motivo da escolha desta definição foi um trabalho recente de refatorização do código que
esta sofreu, o que fez com que a sua estrutura interna fosse modificada com o intuito de tornar o
código menos complexo e mais fácil de manter.
No contexto do sistema indicado acima, esta definição surge numa funcionalidade relacionada
com o processamento das licitações feitas pelos trabalhadores, onde estes definem a sua preferência
no que toca às linhas da escala de um plano de trabalho. Ao realizar este processamento, a definição
57
tem em conta as preferências dos trabalhadores assim como um conjunto de regras definidas pela
empresa.
Este processo decorre da seguinte maneira:
• Criação das linhas de escala;
• Abertura do processo de licitação;
• Escolha das linhas de escala, por parte dos trabalhadores, de acordo com a sua preferência;
• Processamento das licitações dos recursos;
• Atribuição das linhas da escala.
No processo de análise da complexidade ciclomática, a definição original, que foi a primeira
implementação do algoritmo de licitações, continha 120 linhas de código e resultou numa
complexidade de 35 (133 caminhos e 100 nodos).
V(G) = E – N + 2
V(G) = 133 – 100 + 2
V(G) = 35
Apesar de ser uma definição complexa e apresentar um alto risco no que diz respeito à
probabilidade da ocorrência de falhas no processo de testes, o seu conteúdo não era muito rico, pois
consistia muito na utilização de condicionais relativamente simples e atribuição de valores a variáveis.
De uma forma simples, o processo descrito neste algoritmo tratava de verificar a validade de
determinada condição, sendo que em caso afirmativo realizavam-se algumas atribuições, caso
contrário, testava-se outra condição.
Da análise do grafo do Anexo III – Grafo do caso de estudo III (original) e da Figura 28, pode
ver-se que este era um algoritmo bastante confuso, pois existiam várias condições encadeadas e
muitos caminhos possíveis, o que tornava a tarefa de testes bastante complicada, assim como a
própria manutenção do código devido a sua complexidade.
58
Figura 28 - Exemplo de parte do grafo da definição original do caso III.
Este algoritmo, tendo sido implementado por um analista-programador com menor
experiência, continha alguma complexidade que não era necessária e até mesmo indesejável, tendo
surgido então a necessidade de refatorizar o código.
O grafo desta nova definição, após refatorização, pode ser consultado no Anexo IV – Grafo do
caso de estudo III (refatorizado). Com esta refatorização, o novo algoritmo obteve um valor de
complexidade ciclomática de 14.
Parte da estratégia de refatorização consistiu em remover algumas das responsabilidades do
corpo da função principal criando três novas definições, representadas na figura em baixo por
“Função”, “Função_X” e “Função_Y”, com complexidades de 2, 2 e 3, respetivamente.
59
Figura 29 - Grafos das definições criadas.
Estas novas definições, são definições relativamente simples e que fazem cálculos muito
específicos. A primeira apenas verifica se deve usar a “funcao_X” ou a “funcao_Y” para fazer o
cálculo, e essas funções fazem os cálculos específicos sobre que linha de escala e que semana devem
ser atribuidas aos recursos que licitaram, dependendo da sua antiguidade na empresa.
A tabela seguinte é um resumo dos valores de complexidade da definição refatorada e das
novas definições criadas.
V(G) = E – N + 2: V(G) calculada:
Definição refatorada V(G) = 42 – 30 + 2 V(G) = 14
Definição 1 V(G) = 8 – 8 + 2 V(G) = 2
Definição 2 V(G) = 8 – 8 + 2 V(G) = 2
Definição 3 V(G) = 12 – 11 + 2 V(G) = 3
Tabela 6 - Resumo sobre o cálculo da complexidade das novas definições.
60
Como se pode observar pela Tabela 6, houve uma diminuição drástica na complexidade
ciclomática da definição analisada, pois esta passou de 35 para 14, e no que toca a linhas de código
de 120 para 31 linhas.
Mesmo juntando a complexidade da definição de processamento de licitações refatorizada com
todas as pequenas definições criadas, obtém-se uma complexidade total de 21, resultante de
14+(2+2+3)*n, sendo n o número de vezes que as novas funções são chamadas (neste caso é igual a
1), o que continua a ser menor que a inicial.
Além desta refatorização ter trazido vantagens no que toca à manutenção do código, foi também
uma preciosa ajuda para os testes, pois estando estes pequenos módulos separados, tornou-se mais
fácil testar cada um deles.
61
7. CONCLUSÕES
7.1 Síntese dos resultados alcançados
Nos dias de hoje, existe uma grande preocupação com a qualidade do software, sendo a alta
complexidade do mesmo considerada um fator indesejável. Sabendo que esta está diretamente ligada
à produtividade dos recursos que trabalham na manutenção de software, onde eu me incluo, foi um
fator de interesse e uma motivação extra ter realizado o projeto nesta área.
Esta área tem sido alvo de estudos constantes, tendo sido feito um investimento considerável
no campo das métricas de avaliação, com o intuito de fornecer ferramentas que auxiliem a análise de
complexidade de software, assim como a diminuição de complexidade resultante desse processo.
Estas métricas de avaliação visam proporcionar uma forma de medir a complexidade e
também a efetividade de um sistema.
Neste contexto, foi escolhida para este projeto a métrica da complexidade ciclomática, que veio
trazer uma abordagem baseada na teoria de grafos e que se adequava bastante bem ao propósito
deste projeto. Não sendo este um processo fácil de realizar manualmente quando se está a lidar com
software bastante complexo, como é o caso dos produtos da SISCOG, foi necessário implementar uma
ferramenta que fizesse esta análise automaticamente e sem grande esforço por parte dos analistas-
programadores. Esta ferramenta é apenas para ser usada internamente na SISCOG, não sendo por
isso divulgada em código aberto.
Para este projeto foi então necessário realizar um estudo das temáticas inerentes à
complexidade ciclomática, nomeadamente os testes de software, no qual foi abordado o papel dos
testes na indústria de software e alguns dos seus tipos, como os testes de caixa branca e os testes de
caixa preta, assim como uma parte da teoria de grafos que engloba todos os conceitos necessários
para a complexidade ciclomática e o papel dos grafos de controlo de fluxo na sua medição.
No que toca à implementação da ferramenta, houve um certo cuidado em tentar modularizar
os vários componentes do código, tornando o mesmo mais fácil de manter e compreender. Existem,
contudo, ainda alguns componentes que precisam de ser revistos pois apresentam alguma
complexidade.
O processo de utilização da ferramente é bastante simples, já que o utilizador apenas tem de
invocar uma definição, que automaticamente analisa o código e constrói um relatório com os
resultados. Neste relatório, é possível ver pelo grafo onde se concentram as partes mais complexas do
62
código, podendo depois o utilizador focar aí a sua atenção para proceder à análise na tentativa de
reduzir a complexidade.
Nos desafios encontrados durante este projeto, o primeiro a destacar foi a escolha da
linguagem LISP, pois era uma linguagem relativamente nova para mim e sendo esta bastante rica e
flexível, acabou por colocar vários desafios durante o desenho e implementação da ferramenta.
Outro dos principais desafios deste projeto foi a utilização do code walker, pois a
documentação do mesmo é praticamente inexistente, o que levou a que em alguns pontos, fosse
necessário recorrer a uma abordagem de tentativa-erro para perceber como este funcionava e o que se
podia extrair do mesmo. O code walker trouxe também outros desafios por causa da forma como este
lê os vários operadores, macros e todas as definições especificadas no LISP, pois por vezes, este tem
um comportamento diferente no caso destas estarem isoladas ou dentro de outras definições.
Na parte de implementação propriamente dita, o maior desafio foi validar a imensidão de
caminhos possíveis capazes de ser gerados dentro das definições. Para contornar este problema, mais
uma vez, foi necessário recorrer à uma abordagem de tentativa-erro, usando casos cada vez mais
complexos para assim tentar cobrir o máximo de caminhos possíveis. Neste âmbito foi também
necessário recorrer por vezes à especificaçãoo da linguagem para conseguir compreender a linguagem
de uma forma mais completa.
Para avaliar os resultados desta ferramenta analisaram-se três casos de estudos, começando
por uma definição pouco complexa e evoluindo posteriormente para casos mais complexos.
Sendo que o primeiro caso de estudo serviu apenas como introdução, pois a sua complexidade era
baixa e o segundo caso de estudo foi usado por ser bastante rico sintaticamente e ter já alguma
complexidade, o caso que se pode considerar verdadeiramente como o caso de estudo deste projeto é
o terceiro.
Este caso foi escolhido por ser uma definição que já sofreu uma reformulação, podendo aí ser
comparada a complexidade ciclomática antes e depois dessa intervenção. Se antes, a definição era
bastante complexa e como se pode observar pelo seu grafo, existiam diversas condições encadeadas, o
depois, reflete uma forte redução da complexidade pois, pelo mais baixo valor da complexidade
ciclomática e pelo grafo mais simples, nota-se que já é uma definição mais fácil de compreender e de
manter. Sendo que esta definição não sofreu alterações de comportamento durante a reformulação,
pode-se concluir que existia uma maior complexidade que era desnecessária e que estava intimamente
ligada a decisões tomadas pelo analista-programador.
63
Com esta análise e analisando todos os outros resultados deste projeto, pode-se concluir que
então que software com complexidade ciclomática alta é difícil de compreender, difícil de manter e é
geralmente sinal de modularização inadequada ou então da existência de muita lógica inserida na
mesma definição.
Acabado este projeto, espera-se que com o uso da ferramenta, os analistas-programadores
tenham uma nova ferramenta de suporte durante a análise do código produzido e que a equipa de
testes possa utilizar a mesma como apoio na contabilização do número de casos de testes a desenhar.
7.2 Trabalho futuro
Acabada a primeira fase deste projeto, podem ser considerados vários aspetos a ser trabalhados no
futuro.
Relativamente à ferramenta em si, vai ser necessário continuar o trabalho de modularização de
algumas partes da ferramenta com o intuito de torná-las mais fáceis de compreender e manter. Vai
também ser necessário continuar a extender a ferramenta para outros casos ligados a especificidades
da linguagem LISP que possam não ter sido contemplados e sejam entretanto detetados.
Uma ideia interessante que surgiu durante o desenvolvimento deste projeto foi introduzir a
ferramenta no mecanismo de compilação do código, tornando o processo totalmente automático, isto
é, o valor ca complexidade ciclomática passaria a ser obtido junto com a mensagem de compilação.
Por outro lado, seria também interessante, analisar estratégias que permitissem fazer uma
análise da complexidade mais completa, nevegando até um determinando nível pelas definições que
são utilizadas ao longo do código que está a ser analisado, para que se consiga obter a complexidade
total de uma dada funcionalidade, sendo essa complexidade representada pela complexidade da
definição principal e de todas as sub-definições analisadas. Apesar da métrica original não contemplar
estes casos, pois só considera módulos individuais, existem estudos que sugerem outras abordagens
de adaptação da complexidade ciclomática a estes casos.
Por fim, surgiram também outras ideias que passavam pelo cálculo da complexidade por
“mouse-over”, isto é, calcular automaticamente a complexidade ao passar o rato sobre determinada
definição; e pela navegação dos nodos do grafo para o código, podendo o utilizador carregar num nodo
e ir direto para o código relativo a essa nodo.
65
BIBLIOGRAFIA
Abran, A. (2010). Software Metrics and Software Metrology, Wiley-IEEE Computer Society Press, July.
Abran, A., Lopez, M., & Habra, N., (2004). An Analysis of the McCabe Cyclomatic Complexity Number,
14th International Workshop on Software Measurement (IWSM2004).
Arthur, L.J., (1985). Measuring programmer productivity and software quality. Wiley-lnterscience.
Baker, A.L., & Zweben, S., (1980). A comparison of measures of control flow complexity. IEEE
Transactions on Software Engineering, SE-6,(6),pp. 506 – 511.
Bandara, P.L.M.K., Wikramanayake, G.N., & Goonethillake, J.S., (2009). Software Reliability Estimation
Based on Cubic Splines. Proceedings of the World Congress on Engineering.
Banker, R.D., Srikant, M.D., Kemerer, C.F., & Zweig, D., (1993). Software complexity and maintenance
cost. Communications of the ACM, Vol. 36, No. 11, pp. 81–94.
Basili, V.R., & Hutchens, D.H., (1983). An empirical study of a syntactic complexity family. IEEE
Transactions on Software Engineering, SE-9, (6),pp. 664-672
Basili, V.R., & Perricone, B.T., (1983). Software errors and complexity: An empirical investigation.
Communications of the ACM, Vol. 27, No. 1, pp. 42–52.
Berge, C., (1973). Graphs and Hypergraphs. North-Holland Publishing Company.
Berge, C., (2001). The Theory of Graphs. Dover Publications, September 4.
Boehm, B.W., (1973). Software and its impact: A quantitative assessment. Datamation, vol.19, pp. 48-
59, May.
Butler, L.P., (1983). Software Quality Assurance Cyclomatic Complexity of a Computer Program.
Proceedings of the IEEE 1983 National Aerospace and Electronics Conference, v. 2, pp. 867-73.
Cammack, W.B., & Rogers, H.J., (1973). Improving the programming process. IBM Tech. Rep. TR
00.2483, Oct.
Cardoso, A., (2009). Teoria de Grafos: uma reflexão sobre a sua abordagem no ensino não
universitário. Tese de Mestrado, Universidade Portucalense.
Chowdhury, I., (2009). Using Complexity, Coupling, And Cohesion Metrics as Early Indicators of
Vulnerabilities. Queen’s University Canada, September.
Cobb, G.W., (1978). A measurement of structure for unstructured languages. Proceedings of ACM
SIGMETRICSISIGSOFT, Software Quality Assurance Workshop.
66
Coleman, D., Ash, D., Lowther, B., & Oman, P., (1994). Using metrics to evaluate software system
maintainability. IEEE Computer, vol. 27, no. 8, August.
Curtis, B., (1983). Software metrics: guest editor’s introduction. IEEE Transactions on Software
Engineering, 9, (6), pp.637-638.
Curtis, B., Sheppard, S.B., & Milliman, P., (1979). Third time charm: Stronger prediction of
programmer performance by software complexity metrics. Proceedings of the 4th International
Conference on Software Engineering, pp.356–360.
Curtis, B., Sheppard, S.B. ; Milliman, P. ; Borst, M.A. ; Love, T., (1979). Measuring the psychological
complexity of software maintenance tasks with the Halstead and McCabe metrics. IEEE
Transactions on Software Engineering, SE-5,(2),pp.96 – 104.
De Marco, T., (1982). Controlling software projects: management, measurement and estimation.
Yourdon Press.
Del Grosso, C., Antoniol, G., Merlo, E., & Galinier, P. (2008). Detecting buffer overflow via automatic
test input data generation. Computers & Operations Research, 35(10), 3125–3143.
doi:10.1016/j.cor.2007.01.013.
Dunsmore, H.E., (1984). Software metrics: an overview of an evolving methodology. Information
Processing & Management, 20, (1-2), pp.183-192.
Enescu, N.I., Mancas, D., Manole, E.I., & Udristoiu, S., (2008). Evaluating the correlation between the
increasing of the correctness level and McCabe complexity. Proceedings of the 8th WSEAS
International Conference on Applied Computer Science (ACS'08).
Enescu, N.I., Mancas, D., Manole, E.I., & Udristoiu, S., (2009). Increasing Level of Correctness in
Correlation with McCabe Complexity. International Journal of Computers, Vol. 3, pp. 63-74.
Evans, M.W., & Marciniak, J., (1987). Software Quality Assurance and Management. John Wiley and
Sons, Inc.
Fenton, N.E., & Neil, M., (1999). Software metrics: success, failures and new directions. Journal of
Systems and Software - Special issue on invited articles on top systems and software engineering
scholars, Volume 47 Issue 2-3, July 1, 1999, 149 – 157
Fenton, N.E., Whitty, R. W., & Kaposi, A.A., (1985). A generalised mathematical theory of structured
programming. Theor. Comput. Sci., 36, 145–171.
Garg, A., (2014). An approach for improving the concept of Cyclomatic Complexity for Object-Oriented
Programming. CoRR - Computing Research Repository.
67
Gaur, M. (2013). Software Security-Static Buffer Overflow Analysis in Object Oriented Programming
Environment-A Comparative Study. IJCAIT 2(1): 1-8.
Geoffrey, K., (1991). Cyclomatic Complexity Density and Software Maintenance Productivity. IEEE
Transactions on Software Engineering, vol. 17, no. 12, December.
Gill, G.K., & Kemerer, C.F., (1991). Cyclomatic complexity density and software maintenance
productivity. IEEE Transactions on Software Engineering, vol.17, no.12, pp.1284-1288, Dec
1991. doi: 10.1109/32.106988.
Gold, R., (2010). Control flow graphs and code coverage. Int. J. Appl. Math. Comput. Sci., 2010, Vol.
20, No. 4, 739–749. DOI: 10.2478/v10006-010-0056-9.
Gold, R., (2013). Decision Graphs and Their Application to Software Testing. ISRN Software
Engineering, vol. 2013, Article ID 432021, 12 pages, 2013. doi:10.1155/2013/432021
Gollhofer, M., (1983). Predicting Errors using McCabe's Metric, Master's Thesis, University of California,
Davis.
Grady, R.B, (1992). Practical Software Metrics for Project Management and Process Improvement.
Prentice-Hall.
Hansen, W.J., (1978). Measurement of program complexity by the pair (cyclomatic number, operator
count). SIGPLAN Notices, 13, (3), pp. 29 - 33
Harrison, W., & Magel, K., (1981). A complexity measure based on nesting level. SIGPLAN Notices, 16,
(3), pp. 63 – 74
Harrison, W., Magel, K., Kluczny, R., & De Kock, A., (1982). Applying software complexity metrics to
program maintenance. Computer, 15, Sept., pp. 65 - 79
Henderson-Sellers, B. (1996), Object-Oriented Metrics. Measures of Complexity , Prentice Hall.
Henry, S., Kafura, D., & Harris K., (1981). On the relationship among three software metrics.
Performance Evaluation Review, v. 10,
IEEE (1990). An Empirical Evaluation (and Specification) of the all-du-paths. IEEE Standard Computer
Dictionary: A Compilation of IEEE Standard Computer Glossaries.
Iyengar, S.S., Parameswaran, N., & Fuller, J., (1982). A measure of logical complexity of programs.
Computer Languages, 1982, 7, pp. 147 – 160
Jalote, P. (2005). An Integrated Approach to Software Engineering. Springer, New York, NY.
Jay, G., Hale, J.E., Smith, R.K., Hale, David, Kraft, N.A., & Ward, C. (2009). Cyclomatic Complexity and
Lines of Code: Empirical Evidence of a Stable Linear Relationship. J. Software Engineering &
Applications, 2: 137-143. doi:10.4236/jsea.2009.23020
68
Jovanović, I., (2006). Software Testing Methods and Techniques. The IPSI BgD Transactions on Internet
Research: 30.
Kanewala, U., & Bieman, J. M. (2014). Testing scientific software: A systematic literature review.
Information and Software Technology, 56(10), 1219–1232. doi:10.1016/j.infsof.2014.05.006
Khalid, M., ul Haq, S., Khan, M.N.A., (2013). An Assessment of Extreme Programming Based
Requirement Engineering Process. International Journal of Modern Education and Computer
Science (IJMECS) 5(2): 41.
Kitchenham, B.A., (1981). Measures of programming complexity. ICL Technical Journal, 2, (3),pp. 298-
316
Kosaraju, S. (1973). Analysis of structured programs. Proceedings of the 5th Annual ACM Symposium
on Theory of Computing, Austin, TX, USA, pp. 240–252.
Krusko, A. (2002). Complexity Analysis of Real Time Software. Royal Institute of Technology Sweden.
Madi, A., Zein, O., & kadry, S., (2013). On the important of cyclomatic complexity metric. International
journal of software engineering and its application, Vol.7, No.2, 2013, pp.67-82.
Magel, K., (1981). Regular expressions in a program complexity metric. SIGPLAN Notices, 16, (7). pp.
61 - 65
McCabe, T. J., (1976). A complexity measure. IEEE Transactions on Software Engineering, 2(4), 308-
320.
McCabe, T., McCabe Jr., T., & Fiondella, L., (2012). Uncovering Weaknesses in Code With Cyclomatic
Path Analysis. CrossTalk - The Journal of Defense Software Engineering, Volume: 25, Issue: 4, Pages:
9-14
McCabe, T.J., Wallace, D., Watson, A.H., (1996). Structured Testing: A Software Testing Methodology
Using the Cyclomatic Complexity Metric. NBS Special Publication, 500-99.
McCarthy, J., (1960). Recursive functions of symbolic expressions and their computation by machine
(Part I). Communications of the ACM, in April.
Meals, R.R., (1981). An Experiment in the Implementation and Application of Halstead's and McCabe's
Measures of Complexity. Software.
Morgado, E.M., & Martins, J.P. (1998). CREWS_NS: Scheduling Train Crew in the Netherlands. AI
Magazine 19 (1): 25-38.
Morgado, E.M., Martins, J.P., & Haugen, R. (2003). TPO: A System for Scheduling and Managing Train
Crew in Norway. Paper published by AAAI.
69
Munson, J.C., & Khoshgoftaar, T.M., (1992). The detection of fault-prone programs. IEEE Transactions
on Software Engineering, Vol. 18, No. 5, pp. 423–433.
Munson, J.C., & Khoshgoftaar T.M., (1989). The dimensionality of program complexity. Proceedings of
the 11th InternationalConference on Software Engineering, pp.245–253.
Myers, G.J., (1977). An extension to the cyclomatic measure of program complexity. SIGPLAN Notices,
12, (lo), pp. 61-64
Myers, G., (1989). The Art of Software Testing. Wiley.
Nystedt, S., (1999). Software Complexity and Project Performance, University of Gothenburg.
Oulsnam, G., (1979). Cyclomatic numbers do not measure complexity of unstructured programs.
Information Processing Letters,8,pp. 207 – 211
Ould, M.A., & Unwin, C., (1986). Testing in Software Development. Cambridge University Press, New
York.
Oviedo, E., (1980). Control flow, data flow and program complexity. Proceedings of COMPSAC 80
Conference, Buffalo, NY, USA, Oct., pp. 146 – 152
Paige, M., (1980). A metric for software test planning. Proceedings of COMPSAC 80 Conference,
Buffalo, NY, USA, Oct., pp. 499-504
Paige, M. (1977). On partitioning program graphs. IEEE Transactions on Software Engineering, SE-3(6): 386–393.
Paige, M.R., (1975). Program graphs, an algebra, and their implication for programming. IEEE Trans. Softw. Eng., SE-1, 3, Sept., 286–291.
Patelia, R. M, & Vyas, S., (2014). A review and analysis on Cyclomatic Complexity. Orient.J. Comp. Sci.
and Technol;7(3)
Prather, R.E., (1984). An axiomatic theory of software complexity metrics. ComputerJournal,27,(4),pp.
340 - 347
Pressman, R.S., (2010). Software engineering: a practitioner's approach. McGraw-Hill Higher
Education.
70
Rapps, S., & Weyuker, E. (1982). Data flow analysis techniques for test data selection. Proceedings of the 6th International Conference on Software Engineering, Tokyo, Japan, pp. 272–278.
Sarwar, M.M.S., Ahmad, I., & Shahzad, S., (2012). Cyclomatic Complexity for WCF: A Service Oriented
Architecture. IEEE on Frontiers of Information Technology (FIT), 10th International Conference.
Sarwar, M.M.S., Shahzad, S., & Ahmad, I., (2013). Cyclomatic complexity: The nesting problem. Eighth
International Conference on Digital Information Management (ICDIM), pp.274-279, 10-12 Sept.
doi: 10.1109/ICDIM.2013.6693981
Schneidewind, N.F., (1979). An Experiment in Software Error Data Collection and Analysis. IEEE
Transactions on Software Enginerring,
Schneidewind, N.F., (1979). Software metrics for aiding program development and debugging. National
Computer Conference, New York, NY, USA, Jun., AFIPS Conference Proceedings Vol. 48, pp.
989 - 994
Shepperd, M., (1988). A critique of cyclomatic complexity as a software metric. Software Engineering
Journal, Volume 3 Issue 2, March 1988, 30-36.
Shimeall, T., (1990). Cyclomatic Complexity as a Utility for Predicting Software Faults. March.
Shuman, E. A., (1990). Cyclomatic complexity as utility for predicting software faults. Postgraduation’s
thesis, Naval Postgraduate School: California, 96 pgs.
SISCOG, (2013). A story written in Lisp. ECLM'13 conference.
SISCOG - Sistemas Cognitivos, SA. Retrieved September 24, 2015, from http://www.siscog.pt/
Solichah, I., Hamilton, M., Mursanto, P., Ryan, C., & Perepletchikov, M., (2013). Exploration on
software complexity metrics for business process model and notation. 2013 International
Conference on Advanced Computer Science and Information Systems (ICACSIS), pp.31-37, 28-
29 Sept. doi: 10.1109/ICACSIS.2013.6761549
Stetter, F., (1984). A measure of program complexity. Computer Languages, 9, (3), pp. 203 - 210
Suresh, Y., Pati, J., & Rath, S. K. (2012). Effectiveness of Software Metrics for Object-oriented System.
Procedia Technology, 6, 420–427. doi:10.1016/j.protcy.2012.10.050
Tan, L. (2006). The Worst Case Execution Time Tool Challenge 2006: The External Test. Technical
report.
Tanik, M.M. (1980). A comparison of program complexity prediction models. SIGSOFT Software
Engineering Notes, 5, (4), pp. 10 - 16
71
Vinju, J.J., & Godfrey, M.W., (2012). What Does Control Flow Really Look Like? Eyeballing the
Cyclomatic Complexity Metric. 2012 IEEE 12th International Working Conference on Source
Code Analysis and Manipulation (SCAM), pp.154-163, 23-24 Sept. 2012. doi:
10.1109/SCAM.2012.17
Walsh, T.J., (1979). A software reliability study using a complexity measure. AFIPS Conference
Proceedings, v. 48, pp. 761-768, 1979.
Wang, A.S., & Dunsmore, H.E., (1984). Back-to-front programming effort prediction. Information
Processing & Management, 29,(1-2), pp. 139-149
Ward, W.T., (1989). Software Defect Prevention Using McCabe's Complexity Metric. Hewlett-Packard
Journal, v. 40, p. 64, April 1989.
White, L. (1981). Basic mathematical definitions and results in testing. Computer Program Testing,
North-Holland, New York, NY.
Whitmire, S. A., (1997). Object-Oriented Design Measurement. Wiley Computer Publishing, 1 edition.
Wiener, R., & Sincovec, R., (1984). Software engineering with Modula-2 and Ada. Wiley.
Wilkie, F.G., & Hylands, B., (1998). Measuring complexity in C++ application software. Software:
Practice and Experience, Vol. 28, No. 5, pp. 513–546
Woodward, M.R., Hennell, M.A., & Hedley, D.A., (1979). A measure of control flow complexity in
program text. IEEE Transactions on Software Engineering, 5, (1). pp. 45 - 50
Yadav, A., & Khan, R., (2012). Development of Encapsulated Class Complexity Metric. Procedia
Technology 4: 754-760.
Zhu, H., Hall, P.A.V., & May, J.H.R, (1997). Software unit test coverage and adequacy. Computing
Surveys, Vol. 29, No. 4 December 1997.
73
ANEXO I – RELÁTORIO GERADO PARA O CASO I
Figura 30 – Anexo I: Exemplo de um dos relatórios HTML gerados.
74
ANEXO II – GRAFO DO CASO DE ESTUDO II
Figura 31 - Anexo II - Grafo do caso de estudo II (Parte 1).
77
ANEXO III – GRAFO DO CASO DE ESTUDO III (ORIGINAL)
Figura 34 - Anexo III - Grafo do caso de estudo III, original (Parte 1).
79
ANEXO IV – GRAFO DO CASO DE ESTUDO III (REFATORIZADO)
Figura 36 - Anexo IV - Grafo do caso de estudo III, refatorizada (Parte 1).