Existem muitas discussões, artigos e blogs sobre o tema da qualidade do código. As pessoas dizem - use técnicas Testado ! Testar é um 'must have' para iniciar qualquer refatoração! Tudo isso está bem, mas estamos em 2016 e há muitos produtos e bases de códigos ainda em produção, que foram criados há dez, quinze e até vinte anos. Não é segredo que muitos deles possuem código legado com baixa cobertura de teste.
Embora eu sempre goste de estar na vanguarda, ou até mesmo na vanguarda do mundo da tecnologia - engajado em novos projetos e tecnologias -, infelizmente nem sempre é possível e muitas vezes tenho que lidar com sistemas desatualizados. Gosto de dizer que, quando você desenvolve do zero, você atua como um criador, criando uma nova matéria. Mas quando você trabalha com código legado, você é como um cirurgião - você sabe como o sistema funciona em geral, mas nunca tem certeza se o paciente sairá bem da 'operação'. E como é um código legado, não há muitos testes atualizados em que você possa confiar. Isso significa que geralmente uma das primeiras etapas é cobri-lo com testes. Para ser mais preciso, não apenas para fornecer cobertura, mas para desenvolver uma estratégia de cobertura de teste.
Basicamente, o que eu precisava determinar era quais partes (classes / pacotes) do sistema precisávamos cobrir com testes primeiro, onde precisamos de testes de unidade, onde testes de interrogação seriam mais úteis, etc. Existem muitas maneiras de abordar esse tipo de análise e a que usei pode não ser a melhor, mas é semelhante a uma abordagem automática. Uma vez que minha abordagem foi implementada, leva pouco tempo para fazer a análise em si e, mais importante, traz um pouco de diversão para a análise do código legado.
A ideia principal aqui é analisar duas métricas - acoplamento (por exemplo, acoplamento aferente, ou CA) e complexidade (por exemplo, complexidade ciclomática).
A primeira mede quantas classes nossa classe mede, portanto, basicamente nos diz quão próxima uma classe particular está do coração do sistema; quanto mais classes houver que usarem nossa classe, mais importante será cobri-las com testes.
Por outro lado, se uma classe é muito simples (por exemplo, contém apenas constantes), se for usada por muitas outras partes do sistema, não é tão importante criar um teste para ela. É aqui que a segunda métrica pode ajudar. Se uma classe contém muita lógica, a complexidade Ciclomática será alta.
A mesma lógica pode ser aplicada ao contrário; Por exemplo, mesmo que uma classe não seja usada por muitas classes e represente apenas um caso de uso específico, ainda faz sentido cobri-la com testes se seu uso lógico interno for complexo.
No entanto, há uma advertência: digamos que temos duas classes - uma com CA de 100 e complexidade de 2 e outra com CA de 60 e complexidade de 20. Embora a soma das métricas seja maior para a primeira, devemos cubra o segundo primeiro. Isso ocorre porque a primeira classe está sendo usada por muitas outras classes, mas não é muito complexa. Por outro lado, a segunda classe também está sendo usada por muitas outras classes, mas é relativamente mais complexa do que a primeira.
Para resumir: precisamos identificar as classes com alta CA e complexidade ciclomática. Em termos matemáticos, você precisa de uma função de aptidão que possa ser usada como classificação. - f (CA, Complexidade) - cujos valores aumentam junto com CA e Complexidade.
Encontrar ferramentas para calcular CA e complexidade para toda a base de código e fornecer uma maneira simples de extrair essas informações no formato CSV provou ser um desafio. Durante minha pesquisa, encontrei duas ferramentas gratuitas, por isso seria injusto não mencioná-las:
O principal problema aqui é que temos dois critérios - CA e complexidade ciclomática - então precisamos combiná-los e convertê-los em um único valor escalar. Se tivéssemos uma tarefa ligeiramente diferente - por exemplo, encontrar uma classe com a pior combinação de nossos critérios - teríamos um problema clássico de otimização multiobjetivo:
Precisamos encontrar um ponto na chamada frente de Pareto (vermelho na foto acima). O interessante sobre o conjunto de Pareto é que cada ponto do conjunto é uma solução para o teste de otimização. Cada vez que descemos na linha vermelha, precisamos nos comprometer com nossos critérios - se um melhora, o outro piora. Isso é chamado de escalarização e o resultado final depende de como é feito.
Existem muitas técnicas que podemos usar aqui. Cada um tem seus prós e contras. No entanto, os mais populares são escalarização linear e aquele que é baseado em um ponto de referência . O linear é o mais fácil. Nossa função de aptidão será semelhante a uma combinação linear de CA e complexidade:
f (CA, Complexidade) = A × CA + B × Complexidade
onde A e B são alguns coeficientes.
O ponto que representa uma solução para o nosso problema de otimização está na linha (azul na foto abaixo). Precisamente, será a intersecção da linha azul e da frente de Pareto vermelha. Nosso problema original não é exatamente um problema de otimização. Mas precisamos criar uma função de categorização. Vamos considerar dois valores de nossa função de categorização, basicamente dois valores em nossa coluna Rank.
R1 = A ∗ CA + B ∗ Complexidade e R2 = A ∗ CA + B ∗ Complexidade
o que é node.js server-side javascript
Algumas das fórmulas escritas acima são equações de linhas, ainda mais essas linhas são paralelas. Levando mais valores de categorização em consideração, teremos mais linhas e, portanto, mais pontos onde a linha de Pareto se cruza com as linhas azuis (pontilhadas). Esses pontos serão classes correspondentes a um determinado valor categorizado.
Infelizmente, há um problema com essa abordagem. Para qualquer linha (Valor Categorizado), teremos pontos com CA pequeno e Complexidade muito grande (e vice-versa). Isso imediatamente coloca os pontos com uma grande diferença entre os valores da métrica em primeiro lugar na lista, que é exatamente o que queríamos evitar.
A outra forma de fazer a escalarização é baseada no ponto de referência. O ponto de referência é um ponto com os valores máximos de ambos os critérios:
(máx (CA), máx (Complexidade))
A função de fitness será a distância entre o ponto de referência e os pontos de dados:
f (CA, Complexidade) = √ ((CA - CA)2+ (Complexidade − Complexidade)2)
Podemos pensar nessa função de adequação como um círculo com o centro no ponto de referência. O raio, neste caso, é o valor da categorização. A solução para o problema de otimização será o ponto onde o círculo toca a frente de Pareto. A solução para o problema original serão conjuntos de pontos correspondentes aos diferentes raios do círculo, conforme mostrado na imagem a seguir (partes dos círculos para diferentes categorias são mostradas como curvas pontilhadas em azul):
Essa abordagem lida melhor com valores extremos, mas ainda há dois problemas: Primeiro - eu gostaria de ter mais pontos próximos aos pontos de referência para resolver melhor o problema que enfrentamos com a combinação linear. Segundo - CA e complexidade ciclomática são inerentemente diferentes e têm diferentes conjuntos de valores, então precisamos normalizá-los (por exemplo, para que todos os valores de ambas as métricas sejam de 1 a 100)
Aqui está um pequeno truque que podemos aplicar para resolver o primeiro problema - em vez de olhar para CA e complexidade ciclomática, podemos olhar para seus valores invertidos. O ponto de referência neste caso será (0,0). Para resolver o segundo problema, podemos normalizar as métricas usando um valor mínimo. Esta é a aparência:
Complexidade normalizada e invertida - NormComplexity :
(1 + min (Complexidade)) / (1 + Complexidade) ∗ 100
AC invertido e normalizado - NormCA :
(1 + min (CA)) / (1 + CA) ∗ 100
Nota: Eu adicionei 1 para ter certeza de que não há divisão por 0. t
A imagem a seguir mostra um gráfico com valores invertidos:
Chegamos à etapa final - calcular a categorização. Como mencionei, estou usando o método de benchmark, portanto, tudo o que precisamos fazer é calcular o comprimento do vetor, normalizá-lo e destacá-lo com a importância de criar um teste de unidade para uma classe. Aqui está a última fórmula:
Rank (NormComplexity, NormCA) = 100 - √ (NormComplexity2+ NormCA2) / √2
Há mais idéias que gostaria de acrescentar, mas vamos primeiro examinar algumas estatísticas. Aqui está um histograma das métricas do acoplador:
O que é interessante nessa imagem é o número de classes com baixo CA (0-2). As classes com CA em 0 não são usadas ou são serviços de alto nível. Estes representam pontos finais FOGO , então está tudo bem se tivermos muitos deles. Mas as classes com CA em 1 são aquelas que são usadas diretamente pelos terminais e temos mais dessas classes do que terminais. O que isso significa de uma perspectiva de arquitetura / design?
Em geral, isso significa que temos uma abordagem orientada a script - nós fazemos o script de cada caso de negócios separadamente (não podemos reutilizar o código porque os casos de negócios são muito diversos). Se for esse o caso, então é definitivamente um código de cheiro e precisamos refatorar. Caso contrário, significa que a coesão do nosso sistema é baixa, neste caso também precisamos de refatoração, mas de refatoração de arquitetura neste caso.
A informação adicional que podemos obter do histograma acima é que podemos filtrar completamente as classes de baixo acoplamento (CA em {0,1}) da lista de classes disponíveis para cobertura com testes de unidade. As mesmas classes, no entanto, são boas candidatas para testes de integração / funcionais.
Você pode encontrar todos os scripts e recursos que usei neste repositório GitHub: ashalitkin / code-base-stats .
Não necessariamente. Em primeiro lugar, trata-se de análise estática, não de tempo de execução. Se uma classe for filtrada de muitas outras classes, pode ser um sinal de que ela é muito usada, mas nem sempre é o caso. Por exemplo, não sabemos se a funcionalidade é muito usada pelos usuários finais. Em segundo lugar, se o design e a qualidade do sistema forem bons o suficiente, certamente diferentes partes / camadas do sistema serão desacopladas nas interfaces, de modo que uma análise de CA estática não nos dará uma imagem real. Acho que essa é uma das principais razões pelas quais o CA não é uma ferramenta popular como o Sonar. Felizmente, para nós está tudo bem, já que, se você se lembra, estamos interessados em aplicar isso especificamente a bases de código antigas e feias.
Em geral, eu diria que a análise de tempo de execução daria melhores resultados, mas infelizmente é muito mais cara, demorada e complexa, então nossa abordagem é potencialmente uma alternativa útil e menos cara.
Relacionado: Responsabilidade primária única: uma receita para um código excelente