November 13th, 2008 § § permalink
Flog é outro analisador de complexidade para Ruby. Diferentemente do Saikuro, que mede a complexidade ciclomática real do código, o Flog tem o propósito de mostrar padrões de tortuosidade no seu código. É mais uma ferramenta interessante e de fácil uso para analisar a evolução da complexidade do seu código:
Instalação
Para instalar o Flog, basta rodar o comando gem:
~$ sudo gem install flog
Esse comando instala a gem em si e um executável que pode ser usado na linha de comando.
Um programa de exemplo
Para demonstrar o uso da ferramenta, vamos utilizar o mesmo exemplo que mostrarmos no artigo sobre o uso do Saikuro.
require "test/unit"
class DocumentHasher
def initialize(object, options = {})
@object = object
@options = options
end
def hash
hash = { :id => @object.id }
if @options[:fields]
@options[:fields].each do |field|
hash[field] = @object.send(field)
end
end
if @options[:extrafields]
@options[:extrafields].each do |field|
hash[field] = @object.send(field.tos + "field")
end
end
hash
end
def self.hash(object, options = {})
self.new(object, options).hash
end
end
class DocumentHasherTest < Test::Unit::TestCase
Struct.new("HashExample", :id, :title, :author)
TITLEFIELD = "Test"
AUTHORFIELD = "Author"
EXTRA_FIELD = "Extra"
def setup
@object = Struct::HashExample.new(1, TITLEFIELD, AUTHORFIELD)
class << @object
def extra_field
"Extra"
end
end
end
def testhashid
result = DocumentHasher.hash(@object)
assert_equal 1, result[:id]
end
def testhashfields
result = DocumentHasher.hash(@object,
:fields => [:title, :author])
assertequal TITLEFIELD, result[:title]
assertequal AUTHORFIELD, result[:author]
end
def testhashextrafields
result = DocumentHasher.hash(@object,
:fields => [:title, :author],
:extrafields => [:extra])
assertequal EXTRAFIELD, result[:extra]
end
end
Salve esse arquivo para um arquivo chamado hasher.rb e rode-o com o comando abaixo para verificar sua execução:
~$ ruby hasher.rb
Utilização
Com esse programa, podemos rodar o Flog. O comando é simples:
~$ flog hasher.rb
Resultado inicial
O resultado da execução acima pode ser visto na listagem abaixo:
Total Flog = 57.7 (7.2 +/- 62.8 flog / method)
DocumentHasher#hash: (27.3)
9.0: send
8.6: assignment
5.8: []
5.4: branch
2.8: each
1.9: to_s
1.7: +
1.5: new
1.3: hash
1.3: id
DocumentHasherTest#setup: (8.3)
6.5: sclass
1.3: assignment
1.3: new
0.4: lit_fixnum
Note que o método hash possui um fator de "tortuosidade" de 27 e que os maiores culpados são o envio de mensagens (send) e atribuições. A "tortuosidade" total do programa está em 57.7, que representa o cômputo de todos os métodos do programa.
Alterando o programa
Para fins de exemplo, podemos tentar reduzir a complexidade do método hash como fizemos no artigo anterior usando o exemplo abaixo:
require "test/unit"
class DocumentHasher
def initialize(object, options = {})
@object = object
@options = options
end
def hash
hash = { :id => @object.id }
build_fields(hash, @options[:fields] || {})
build_extra_fields(hash, @options[:extra_fields] || {})
hash
end
def self.hash(object, options = {})
self.new(object, options).hash
end
protected
def build_fields(hash, fields)
fields.each do |field|
hash[field] = @object.send(field)
end
end
def build_extra_fields(hash, fields)
fields.each do |field|
hash[field] = @object.send(field.to_s + "_field")
end
end
end
class DocumentHasherTest < Test::Unit::TestCase
Struct.new("HashExample", :id, :title, :author)
TITLE_FIELD = "Test"
AUTHOR_FIELD = "Author"
EXTRA_FIELD = "Extra"
def setup
@object = Struct::HashExample.new(1, TITLE_FIELD, AUTHOR_FIELD)
class << @object
def extra_field
"Extra"
end
end
end
def test_hash_id
result = DocumentHasher.hash(@object)
assert_equal 1, result[:id]
end
def test_hash_fields
result = DocumentHasher.hash(@object,
:fields => [:title, :author])
assert_equal TITLE_FIELD, result[:title]
assert_equal AUTHOR_FIELD, result[:author]
end
def test_hash_extra_fields
result = DocumentHasher.hash(@object,
:fields => [:title, :author],
:extra_fields => [:extra])
assert_equal EXTRA_FIELD, result[:extra]
end
end
Rodando o comando novamente, teríamos:
Total Flog = 58.1 (5.3 +/- 10.3 flog / method)
DocumentHasher#hash: (10.7)
3.2: []
3.0: branch
2.6: assignment
1.5: new
1.3: build_extra_fields
1.3: hash
1.3: build_fields
1.3: id
DocumentHasher#build_extra_fields: (9.4)
4.2: send
2.8: assignment
1.8: to_s
1.6: +
1.3: branch
1.3: each
DocumentHasherTest#setup: (8.3)
6.5: sclass
1.3: assignment
1.3: new
0.4: lit_fixnum
DocumentHasherTest#test_hash_fields: (7.0)
3.0: []
2.6: assert_equal
1.3: hash
1.3: assignment
Note que a complexidade dos métodos caiu substancialmente. Os métodos estão mais simples e mais balanceados. A "tortuosidade" geral subiu um pouco com a adição de algumas chamadas, mas, analisando os métodos individualmente é possível observar que a somatória de complexidade dos três métodos envolvidos é menor do que a complexidade original. Mais uma vez, o código está mais legível e mais testável.
Conclusão
O Flog representa uma métrica arbitrária de complexidade que, mesmo assim, pode ser usada para acompanhar o desenvolvimento de um base de código. Um exemplo disso pode ser visto no texto do Carlos Villela mostrando a evolução do código do Rails ao longo dos anos.
Como de usual, sugestões, críticas e dúvidas são bem-vindos. No próximo artigo, esfolando o seu código Ruby com mais ferramentas de análise de complexidade.
November 13th, 2008 § § permalink
Flay é uma ferramenta útil para cercear repetições de código em programas Ruby. Analisando o código semanticamente, o Flay é capaz de pegar repetições em casos que geralmente passam despercebidos no melhor dos códigos.
Em algumas casos, a repetição será inevitável e mesmo necessária para deixar o código mais legível. Geralmente, entretanto, repetição significa código que ainda não está DRY o suficiente.
Instalação
Para instalar o Flay, basta rodar o comando gem:
~$ sudo gem install flay
Esse comando instala a gem em si e um executável que pode ser usado na linha de comando.
Um exemplo
Para demonstrar o uso da ferramenta, vamos rodar o Flay contra o código mais recente do Active Record:
O resultado (um trecho apenas), depois de alguns minutos de trabalho com a CPU a 100%, é o seguinte:
Matches found in :defn (mass = 290)
./test/cases/associations/join_model_test.rb:237
./test/cases/associations/join_model_test.rb:262
./test/cases/associations/join_model_test.rb:271
./test/cases/associations/join_model_test.rb:623
./test/cases/associations/join_model_test.rb:650
Matches found in :defn (mass = 270)
./test/cases/validations_test.rb:929
./test/cases/validations_test.rb:937
./test/cases/validations_test.rb:945
./test/cases/validations_test.rb:953
./test/cases/validations_test.rb:961
./test/cases/validations_test.rb:969
Matches found in :defn (mass = 236)
./test/cases/validations_i18n_test.rb:491
./test/cases/validations_i18n_test.rb:510
./test/cases/validations_i18n_test.rb:529
./test/cases/validations_i18n_test.rb:549
Matches found in :scope (mass = 204)
./test/migrations/duplicate/3_innocent_jointable.rb:12
./test/migrations/interleaved/pass_1/3_innocent_jointable.rb:12
./test/migrations/interleaved/pass_2/3_innocent_jointable.rb:12
./test/migrations/interleaved/pass_3/3_innocent_jointable.rb:12
./test/migrations/missing/4_innocent_jointable.rb:12
./test/migrations/valid/3_innocent_jointable.rb:12
Matches found in :call (mass = 172)
./test/models/author.rb:64
./test/models/project.rb:17
Matches found in :defn (mass = 171)
./test/cases/validations_test.rb:977
./test/cases/validations_test.rb:991
./test/cases/validations_test.rb:1079
Note especialmente código que está sendo repetido em arquivos diferentes. No caso acima, como o código está em arquivos de teste, é provável que a repetição seja desejável para não poluir o escopo dos mesmos.
Vamos tomar um outra caso na biblioteca em si:
Matches found in :if (mass = 165)
./lib/active_record/associations/has_and_belongs_to_many_association.rb:82
./lib/active_record/associations/has_many_association.rb:100
./lib/active_record/associations/has_many_through_association.rb:194
O código repetido em questão é o seguinte:
if @reflection.options[:counter_sql]
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
elsif @reflection.options[:finder_sql]
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
else
@counter_sql = @finder_sql
end
Vendo os arquivos, é possível observar que o código é o mesmo. Considerando que os três arquivos representam classes com um pai em comum seria possível reduzir a repetição movendo o código para um método a classe pai.
Esse tipo de repetição tende a sumir com refatoramentos e o código acima provavelmente sumirá por um processo similar (alguém se habilita para um patch?).
Conclusão
Como é possível perceber, o Flay representa mais uma forma de observar o seu código em busca de coisas que podem ser melhoradas. É claro que isso não deve ser converter em uma obsessão de remover toda e qualquer repetição, o que demoraria mais tempo do que codificar coisas novas.
Como de usual, sugestões, críticas e dúvidas são bem-vindos.
November 12th, 2008 § § permalink
Saikuro é uma analisador de complexidade ciclomática para Ruby. A instalação e uso do mesmo são bem simples e serão descritos aqui brevemente.
Instalação
Para instalar o Saikuro, basta rodar o comando gem:
~$ sudo gem install Saikuro
Esse comando instala a gem em si e um executável que pode ser usado na linha de comando.
Um programa de exemplo
Para demonstrar o uso da ferramenta, vamos utilizar um programa de referência. Esse programa será um simples transformador de objetos Ruby em hashes equivalentes e está listado abaixo, com os testes necessários:
require "test/unit"
class DocumentHasher
def initialize(object, options = {})
@object = object
@options = options
end
def hash
hash = { :id => @object.id }
if @options[:fields]
@options[:fields].each do |field|
hash[field] = @object.send(field)
end
end
if @options[:extrafields]
@options[:extrafields].each do |field|
hash[field] = @object.send(field.tos + "field")
end
end
hash
end
def self.hash(object, options = {})
self.new(object, options).hash
end
end
class DocumentHasherTest < Test::Unit::TestCase
Struct.new("HashExample", :id, :title, :author)
TITLEFIELD = "Test"
AUTHORFIELD = "Author"
EXTRA_FIELD = "Extra"
def setup
@object = Struct::HashExample.new(1, TITLEFIELD, AUTHORFIELD)
class << @object
def extra_field
"Extra"
end
end
end
def testhashid
result = DocumentHasher.hash(@object)
assert_equal 1, result[:id]
end
def testhashfields
result = DocumentHasher.hash(@object,
:fields => [:title, :author])
assertequal TITLEFIELD, result[:title]
assertequal AUTHORFIELD, result[:author]
end
def testhashextrafields
result = DocumentHasher.hash(@object,
:fields => [:title, :author],
:extrafields => [:extra])
assertequal EXTRAFIELD, result[:extra]
end
end
Salve esse arquivo para um arquivo chamado hasher.rb e rode-o com o comando abaixo para verificar sua execução:
~$ ruby hasher.rb
Utilização
Com esse programa, podemos rodar o Saikuro. O comando é simples:
~$ saikuro -c -t -y 0 -w 11 -e 16 -i hasher.rb -o report
Esse comando roda o analisador de complexidade com os seguintes parâmetros:
- -t, para analisar tokens também
- -y 0, para exibir a complexidade de todos os métodos
- -w 11, para exibir warnings para métodos de complexidade superior a 11
- -e 16, para exibir erros para métodos de complexidade superior a 16
- -i hasher.rb, o arquivo a ser analisado (pode ser um diretório também)
- -o report, o diretório onde gerar o relatório de saída
Resultado inicial
O resultado da execução acima pode ser visto, parcialmente, na imagem abaixo, que mostra os resultados somente para a classe em que estamos interessados:

Note que o método hash possui complexidade 5 que reflete os 4 comandos decisórios (2 if e 2 each) mais um ponto de saída pelo cálculo mais simples da complexidade. Os demais métodos, puramente lineares, possuem complexidade 1.
No diretório de resultado há também um arquivo que mostra a quantidade de tokens gerados no programa. Este relatório, com os níveis de aviso baixo, tende a reportar erros mesmo em condições consideradas normais.
Alterando o programa
Para fins de exemplo, podemos tentar reduzir a complexidade do método hash. Digamos que façamos a seguinte alteração:
require "test/unit"
class DocumentHasher
def initialize(object, options = {})
@object = object
@options = options
end
def hash
hash = { :id => @object.id }
build_fields(hash, @options[:fields] || {})
build_extra_fields(hash, @options[:extra_fields] || {})
hash
end
def self.hash(object, options = {})
self.new(object, options).hash
end
protected
def build_fields(hash, fields)
fields.each do |field|
hash[field] = @object.send(field)
end
end
def build_extra_fields(hash, fields)
fields.each do |field|
hash[field] = @object.send(field.to_s + "_field")
end
end
end
class DocumentHasherTest < Test::Unit::TestCase
Struct.new("HashExample", :id, :title, :author)
TITLE_FIELD = "Test"
AUTHOR_FIELD = "Author"
EXTRA_FIELD = "Extra"
def setup
@object = Struct::HashExample.new(1, TITLE_FIELD, AUTHOR_FIELD)
class << @object
def extra_field
"Extra"
end
end
end
def test_hash_id
result = DocumentHasher.hash(@object)
assert_equal 1, result[:id]
end
def test_hash_fields
result = DocumentHasher.hash(@object,
:fields => [:title, :author])
assert_equal TITLE_FIELD, result[:title]
assert_equal AUTHOR_FIELD, result[:author]
end
def test_hash_extra_fields
result = DocumentHasher.hash(@object,
:fields => [:title, :author],
:extra_fields => [:extra])
assert_equal EXTRA_FIELD, result[:extra]
end
end
Rodando o comando novamente, teríamos:

Como os testes demonstram, o programa funciona da mesma forma. E como o relatório de complexidade também demonstra, a complexidade do programa permanece a mesma no geral e a dos métodos diminuiu. O resultado é um programa mais legível e mais facilmente testável.
Conclusão
Embora métricas em si não tenham nenhum poder de tornar o código de um programa melhor, acompanhar a evolução das mesmas é uma ferramenta fundamental para garantir a qualidade do código. Para quem usa o Ruby, o Saikuro é uma ferramenta simples e rápida para isso. Para quem não usa, é fácil achar ferramentas que se adequam a outras linguagens.
Como de usual, sugestões, críticas e dúvidas são bem-vindos. No próximo artigo, ainda hoje, esfolando o seu código Ruby com mais ferramentas de análise de complexidade.
November 12th, 2008 § § permalink
Este é o quarto artigo em uma série sobre conceitos de programação. Os demais artigos podem ser encontrados na página de resumo sobre a série. Como nos demais artigos, um aviso: o tratamento dado aos assuntos aqui é informativo e podem existir erros nos mesmos. Como nos demais artigos neste blog, comentários e correções são sempre bem-vindos.
Complexidade Ciclomática
Neste quarto artigo, vamos tratar um pouco de complexidade ciclomática. Apesar do nome esdrúxulo, complexidade ciclomática (também conhecida como complexidade condicional) é uma métrica simples para determinar, como o próprio nome sugere, a complexidade de um programa estruturado (cíclico).
Essa métrica foi desenvolvida em 1976 por Thomas J. McCabe e reflete diretamente o número de caminhos independentes que um programa pode tomar durante a sua execução.
Qualquer desenvolvedor que já tenha testado código em sua vida, sabe que a quantidade de testes necessária para exercitar um determinado trecho de código é diretamente proporcional à sua árvore decisória. Em outras palavras, quanto mais caminhos o código puder tomar (seja por meios de condicionais ou loops), maior a quantidade de testes necessários. E como veremos abaixo, há realmente uma relação direta entre a complexidade ciclomática e a cobertura de um código.
Calculando a complexidade ciclomática
Antes de mostrar exatamente como o cálculo pode ser feito, vamos observar algumas coisas em relação a um programa qualquer. Digamos que você esteja desenvolvendo um programa que lhe dê o maior divisor comum entre dois números. Uma fórmula simples é o algoritmo de Euclides que pode ser descrito da seguinte forma:
Dados dois números naturais a e b, verifique se b é zero. Se sim, a é o maior divisor comum entre os mesmos; caso contrário, repita o processo usando b e o resto da divisão de a por b.
Esse algoritmo pode ser expresso pelo seguinte programa em Ruby (note que ele não está em Ruby idiomático):
require "test/unit"
def euclid(m, n)
if n > m
r = m
m = n
n = r
end
r = m % n
while r != 0
m = n
n = r
r = m % n
end
n
end
class EuclidTest < Test::Unit::TestCase
SETS = [[5, 10, 5], [2, 6, 2], [11, 7, 1],
[80, 64, 16], [2, 2, 2]]
def test_euclid
SETS.each do |set|
assert_equal set[2], euclid(set[0], set[1])
end
end
end
Se o programa acima for executado, ele rodará o caso de teste logo abaixo da função que verificará se a mesma está correta. Você pode adicionar mais casos ao conjunto SETS se desejar.
A função euclid pode ser descrita por um grafo simples que conecta os caminhos entre as várias declarações que a mesma contém. Esse grafo é o mostrado abaixo (clique para expandir):

Com base nesse grafo, podemos definir a complexidade ciclomática de um programa da seguinte forma:
CC = A - N + 2C
Nessa fórmula:
- CC é a complexidade ciclomática
- A é o número de arestas do grafo
- N é o número de nós do grafo
- C é o número de componentes conectados
Como se trata de uma função simples com um único ponto de entrada e saída, o número de componentes é 1 e a fórmula pode ser reduzida para:
CC = A - N + 2
Se a função possuísse múltiplos pontos de saída, entretanto, a complexidade ciclomática seria definida como:
CC = A - N + C + R
Nessa fórmula, R é o número de declarações de saída (em Ruby, o número de returns).
Voltando ao grafo mostra na figura, vemos que o mesmo possui 11 nós e 12 arestas, o que nós dá uma complexidade ciclomática de 12 - 11 + 2, ou seja, 3.
Uma outra maneira bem simples de descobrir a complexidade ciclomática é contar o número de loops fechados no grafo (que são formados por condicionais e loops) e somar ao número de pontos de saída. No grafo acima, temos 2 loops fechados (os if e while) e um ponto de saída, resultando no mesmo valor 3 para a complexidade da função.
Uma coisa interessante é que a complexidade permanece a mesma quando a sintaxe de uma linguagem é levada em questão sem alterar a semântica do programa. Tome por exemplo a versão idiomática do algoritmo em Ruby:
def euclid(m, n)
m, n = n, m if n > m
m, n = n, m % n while m % n != 0
n
end
O grafo gerado nesse caso é (clique para expandir):

Node que embora o número de nós e arestas tenha mudado, a relação entre eles não mudou e a complexidade permanece a mesma.
Testando
De uma forma geral, o valor da complexidade ciclomática define um limite superior para a quantidade de testes necessários para cobrir todos os caminhos decisórios no código em questão. Esse é um limite superior já que nem todos os caminhos são necessariamente realizáveis.
Disso se infere que quanto menor a complexidade, menor a quantidade de testes necessários para o método em questão. Esse fato implica em outro curioso: quebra um método em vários reduz a complexidade dos métodos mas aumenta a complexidade geral do código e, de forma geral, mantém a testabilidade do programa completo no mesmo nível.
Referências
Obviamente, já que a complexidade é um valor específico, é possível extrair da mesma uma referência. Baseado no trabalho de McCabe, esses valores de referência são:
- 1-10, métodos simples, sem muito risco
- 11-20, métodos medianamente complexos, com risco moderado
- 21-50, métodos complexos, com risco alto
- 51 ou mais, métodos instáveis de altíssimo risco
Conclusão
Essa foi uma pequena introdução ao assunto com o objetivo de abrir o caminho para artigos posteriores mostrando ferramentas de apoio ao cálculo e monitoramento da complexidade ciclomática. Como de usual, sugestões e correções são bem vindos.