Flog, analisando complexidade em Ruby

November 13th, 2008 § 0 comments § 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.

Flay, eliminando repetições em código Ruby

November 13th, 2008 § 2 comments § 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.

Saikuro, complexidade ciclomática para Ruby

November 12th, 2008 § 0 comments § 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:

Saikuro 1

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:

Saikuro 2

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.

Conceitos de Programação: Complexidade Ciclomática

November 12th, 2008 § 5 comments § 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.

Where am I?

You are currently viewing the archives for November, 2008 at Superfície Reflexiva.