Usem linguagens dinâmicas

August 11th, 2009 § 45 comments § permalink

Senhoras e senhores da classe de 2009:

Usem linguagens dinâmicas.

Se eu pudesse lhes oferecer somente um conselho para suas carreiras futuras de programação, seria o de usar linguagens dinâmicas. Os benefícios a longo prazo das linguagens dinâmicas já foram provados por milhares de programadores enquanto o resto dos meus conselhos não tem qualquer outra base se não minha própria conturbada experiência.

Eu lhes darei esses conselhos agora.

Aproveite o poder e expressividade de uma linguagem homoicônica. Ou esqueça que elas existem. Você nunca vai entender o poder e expressividade de uma linguagem homoicônica até passar quarenta horas acordado depurando um heisenbug. Mas acredite quando eu digo que, daqui a vinte anos, você vai olhar para trás, para todo código que você escreveu e desejar que ele tivesse sido escrito em uma linguagem homoicônica. Seu código atual é elegante, mas nem tanto.

Não se preocupe com a quantidade de linhas que você escreve. Ou preocupe-se, mas saiba que contar linhas de código é tão efetivo quando tentar contar parênteses em Lisp. O tipo de métrica que realmente vai lhe trazer problemas é a quantidade de declarações de tipo presente em seu código–justamente o código que vai falhar em uma madrugada movida a cafeína e fazer você amaldiçoar o compilador com todas as suas forças pelo pretenso sistema de tipagem segura.

Escreva uma linha de código a cada dia que assuste outros programadores.

Comente.

Seja cuidadoso com o código das outras pessoas. Não tolere pessoas que não são cuidadosas com seu código e introduzem problemas de manutenção nas elegantes estruturas que você construiu.

Não use marcações TODO, HACK ou FIXME em seu código.

Não perca tempo em discussões sobre linguagens de programação. Algumas vezes a sua está à frente no índice TIOBE, outras vezes ela está atrás. A corrida para entrega do código é longa e, no final das contas, suas linhas são as únicas quem contam.

Lembre-se dos forks e patches que seu repositório recebeu. Esqueça os comentários sobre a qualidade do código. Se conseguir fazer isso, me diga como.

Jogue fora a documentação obsoleta. Guarde o código antigo.

Faça forks do código alheio.

Não se sinta culpado por ainda não ter aprendido Assembly. Os melhores programadores que eu conheço não aprenderam até precisarem. Alguns dos mais excepcionais programadores que eu conheço preferem não aprender.

Beba café em quantidades moderadas. Seja bondoso com suas mãos. Você vai sentir falta delas quando a LER atacar.

Talvez você escreva um compilador, talvez não. Talvez você escreva um driver para o kernel do Linux, talvez não. Talvez você programe sistemas de inteligência artifical em ML, talvez não. O que quer que você faça, lembre-se que isso é tão relevante quando decidir se você vai usar o Emacs ou o Vi.

Aproveite bem os testes que você escreveu. Use-os da melhor maneira que puder. Não tenha medo dos que os outros dizem sobre TDD ou o que as pessoas pensam sobre BDD. Sanidade no desenvolvimento é a maior ferramenta que você vai ter em toda sua carreira.

Comemore cada build bem sucedido mesmo que você esteja sozinho no datacenter e ninguém mais possa compartilhar sua alegria.

Escreva um Makefile pelo menos uma vez, mesmo que depois nunca mais você vá usar algo similar.

Não leia revistas sobre tecnologias da Microsoft. Elas somente vão deprimir você pela pura estupidez da re-implementação.

Conheça os luminares de programação. Você vai sentir falta de saber o que Alan Turing e Donald Knuth fizeram algum dia. Seja gentil com seus colegas programadores. No futuro, eles são aqueles que provavelmente vão lhe apontar para o código que você precisar no momento certo.

Entenda que linguages aparecem, se tornam populares e desaparecem com a mesma facilidade mas há algumas que você deve prezar. Trabalhe muito para reconhecer as características boas de cada linguagens que você usa porque, quanto mais tempo de programação você tiver, mais vai precisar reconhecer quando e para quê certas técnicas e linguagens servem.

Programe em C durante um tempo, mas abandone a linguagem antes que ela lhe convença que controle manual de memória é uma coisa boa. Programe em Haskell algum tempo, mas abandone a linguagem antes que ela lhe convença que mensagens de erro absurdas são parte do fluxo normal de programação. E lembre-se de aprender uma nova linguagem de quando em quando.

Aceite certas verdades inalienáveis: linguagens do mercado como Java e C# são uma porcaria, tipagem dinâmica é melhor do que tipagem estática e sua carreira de programação vai terminar um dia. E quando ela terminar, você vai fantasiar que, quando você era um pogramador hot shot, linguagens de mercado eram boas–mas só pelo dinheiro–, que tipagem estática era mais segura e que sua carreira não terminaria nunca.

Respeite aqueles cujas carreiras já terminaram porque eles contribuíram bastante para o lugar em que você está.

Não espere que ninguém lhe ensine como programar melhor. Talvez você tenha um bom mentor. Talvez você tenha acesso a bons livros e documentação. Mas você nunca sabe quando essas coisas vão desaparecer.

Tenha uma biblioteca reusável mas não coloque coisa demais nela ou, quando você precisar, vai descobrir que a maioria do código lá está obsoleto ou é horrível demais para ser usado.

Seja cuidadoso com os algoritmos de terceiros que você usa, mas seja paciente com aqueles que os criaram. Algoritmos são como bichos de estimação. As pessoas que os criaram sempre pensam que eles são confiáveis, limpos e rápidos mas a verdade é sempre diferente e eles raramente valem o bytecode que geram.

Mas confie em mim quando eu falo de linguagens dinâmicas.


Melhor desfrutado ao som de “Wear Sunscreen” do qual é uma óbvia paródia.

Programming Talk

June 29th, 2009 § 4 comments § permalink

Sabe a diferença semântica entre class << self e métodos de classe no Ruby? Já programou meta-decorators em Python? Quer implementar OMeta em sua linguagem favorita? Já programou em Nu ou Newspeak? Já está na curva descendente de uso de meta-programação em sua linguagem diária?

Se respondeu sim a alguma dessas questões ou simplesmente gosta de discutir linguagens de programação de maneira mais profunda, junte-se a nós no grupo programming-talk.

Aceitamos newbies de qualquer natureza–já que todos somos iniciantes em alguma coisa–mas não para fazer perguntas idiotas sobre como usar a feature XYZ em seu trabalho diário. Por causa disso, o grupo é invite-only.

Se tem interesse, acesse o grupo e peça um convite. Justifique sua entrada, a propósito (ou a entrada será automaticamente negada). :)

Rails é, definitivamente, o novo ASP

February 13th, 2009 § 14 comments § permalink

Você sabe que uma tecnologia atingiu o ponto de saturação quando alguém tem a capacidade de propor algo CMM-like para ela.

Rails é, definitivamente, o novo ASP–ou Cobol, ou PHP, ou escolha o que você achar melhor. :)

Waterfalling Agile

January 18th, 2009 § 1 comment § permalink

O Luiz Rocha escreve com razão sobre os perigos de confundir Agile com um conjunto de métodos que devem ser seguidos para que o “processo” funcione. É uma coisa natural de organizações–que por extensão são pessoas–procurarem um ponto do balanço entre necessidades discordantes e este é um dos pontos mais propícios para transformar a filosofia Agile em uma metodologia que pouco difere do tradicional Waterfall.

Por outro lado, existe uma tentação muito grande de pensar em Agile com o oposto exato do Waterfall e transformar isso em uma desculpar para não ter nenhum processo ou método coerente de desenvolvimento. É muito comum encontrar empresas imersas na desorganização que descrevem os seus processos como ágeis.

“Não escrevemos documentação, oras. Somos Agile. Preferimos software que funciona a documentação compreensiva. Não é isso o que o manifesto diz?”

É claro que o manifesto termina com a afirmação: That is, while there is value in the items on the right, we value the items on the left more. Isso não impede que tudo seja abandonado em favor de uma pretensa agilidade.

Como o Luiz diz, não é um task board que faz uma empresa ou equipe ágil. Tampouco é a falta de planos em contraponto a uma resposta completamente desorganizada a mudanças.

Agile exige um compromisso com o detalhe:

Para valorizar interação e não processos é necessário entender que o nível de detalhe requerido do primeiro é maior que o do segundo.

Para valorizar software que funciona é preciso uma cultura de qualidade, o que implica em muito mais trabalho do que escrever documentação compreensiva–e isso vai muito além de colocar no quadro de tarefas itens para escrever testes, algo que nada mais é do que transformar o Agile em Waterfall.

Para valorizar a colaboração do cliente é preciso ouvir e raciocinar sobre o que foi ouvido, transformando isso em valor. Negociar contratos é muito mais simples.

Finalmente, para valorizar resposta rápida a mudanças é necessário uma atitude de constante atenção ao que está sendo feito e como isso vai evoluir no futuro. Novamente, seguir um plano é incomparavelmente mais fácil, mesmo que o mesmo seja completamente falho.

Agile não é, e nunca foi, uma opção para aqueles que querem tomar a rota mais fácil. Excelência técnica, cultura de qualidade, atenção ao detalhe–transformar isso em segunda natureza requer anos de prática: o mesmo tipo de esforço que um artesão passa a vida aplicando.

Se você quer algo simples, esqueça Agile. Não é para quem não tem um compromisso com o tempo necessário para que coisas permanentes floresçam.

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.

Algumas dicas para testes melhores

October 27th, 2008 § 3 comments § permalink

O Lucas Húngaro escreveu recentemente sobre a necessidade de abandonar testes quebradiços com uma forma de manter seus testes saudáveis, citando vários exemplos de como isso pode ser feito. Desnecessário dizer, concordo com o que ele disse.

Um conselho dele em particular me parece a melhor consideração sobre o assunto. Falando sobre mocks e stubs, ele diz:

Algumas soluções pra isso são: não utilizar mocks e stubs (o que é um tanto extremista), aumentar o encapsulamento das classes ou aceitar isso e continuar.

Chamo a atenção para a parte final: algumas vezes, você tem simplesmente que aceitar que alguns testes devem ser de certa forma para garantir uma cobertura mais compreensiva.

Dito isso, seguem algumas dicas para testes melhores:

Mocks e Stubs

Uma regra simples que pode ajudar a entender quando usar essas duas técnicas é a seguinte: Se o comportamento esperado deve ser conhecido, use mocks e stubs. Caso contrário, não.

Isso torna fácil perceber que testes de modelos–no caso do Rails, por exemplo–raramente (ou nunca) deve usar essas técnicas. Quando se trata do domínio da aplicação, encapsulamento é uma propriedade básica de sistemas orientados a objetos que não deve ser violada se possível. Obviamente, abstrações vazam; mas isso não deve ser uma desculpa para quebrá-las em testes.

Não teste a abstração imediatamente inferior

Se você está usando Rails, por exemplo, não faz o menor sentido testar a abstração inferior. É muito comum ver testes como:

it "should return all current books" do
  Book.current.proxy_options.should == { :conditions => { :current => true } }
end

Esse tipo de teste verifica se o Rails está funcionando da maneira como deve funcionar e não se o seu código está correto. O resultado é que pequenas mudanças e principalmente a introdução de algum comportamento ortogonal pode quebrar completamente a aplicação enquanto o teste continua a funcionar.

Digamos, por exemplo, que você agora tenha que usar alguma esquema de caching em sua aplicação. No interesse de que a abstração não vaze, você envolve o método acima em alguma abstração que faça isso. O teste acima provavelmente quebrará de imediato, porque está testando a abstração inferior e não o comportamento da aplicação.

O teste correto, nesse caso, seria verificar se a lista retorna é realmente correta, mesmo que isso envolva alguma manipulação de dados que reduza a velocidade do teste.

Use testes de sanidade

Conversamente, é importante também ter alguns testes de sanidade. Às vezes é necessário saber se alguma coisa está declarada, mesmo que a forma final daquela declaração não seja importante.

Um exemplo disso são testes em views. Geralmente, testar se algum conteúdo está aparecendo em uma view é inteiramente desnecessário e pode gerar testes que devem ser refeitos a cada pequena mudança para se adaptar a novas condições.

Mesmo assim, em alguns casos é importante a existência de testes que correm esse risco como forma de testar a sanidade da aplicação; em outras palavras, testar se condições básicas estão sendo supridas.

Não teste nada que não seja público

Uma pergunta muito comum em listas de discussão é como testar métodos privados ou protegidos de uma classe. A reposta geralmente envolve o uso de reflexão e violação de abstrações.

Um exemplo comum seria o código abaixo:

it "should prepare the environment" do
  Manager.send(:prepare_environment)
  Manager.environment.name.should == "development"
end

A resposta correta deveria ser: nunca faça isso. Se seus testes contém alguma necessidade desse tipo, você está violando o encapsulamento e isso indica alguma problema arquitetural mais básico da aplicação. Provavelmente alguma necessidade de separação de dependências que você ainda não identificou.

Nesse casos, funcionalidade básica que envolva código encapsulado pode ser melhor testada pelo seu efeito em outras partes da aplicação.

Cuidado com a ordem dos seus testes

Uma coisa interessante é tentar rodar os seus testes em ordem arbitrária. Existe opções para isso em alguns dos frameworks existentes que podem ajudar a identificar dependências escondidas em seus testes.

Finalizando

Para terminar, esse são apenas alguns dos muitos conselhos possíveis nessa área. Fiquem à vontade para usar a área de comentários do texto para expandir sobre o assunto.

Um conselho final: use seus testes como oportunidades para pensar sobre a sua aplicação. Se você se vê em uma situação em que testar está se convertendo em algo progressivamente mais complicado, alguma coisa está errada na forma como você desenhou seu código. Pare, pense sobre o assunto, e refatore. Seu código e seu tempo agradecem.

Sinatra

August 8th, 2008 § 4 comments § permalink

Estou brincando com o Sinatra esses dias. É um micro-framework/DSL para desenvolvimento Web.

É tão simples que basta instalar uma gem, criar um pequeno arquivo com a definição do que uma URL REST faz, rodar o próprio arquivo e pronto: você tem um servidor Web rodando e respondendo a requisições sem dependências e frescuras. Um exemplo:

require "rubygems"
require "sinatra"

get "/" do
 "Hello, World!"
end

Em um mundo de frameworks cada vez mais complexos, o Sinatra é uma ferramenta bem útil que vale a pena conferir.

Where Am I?

You are currently browsing the Programação category at Superfície Reflexiva.