• Listar posts
  • Feeds - RSS
Post

Flay - um guia sobre o que refatorar

Postado por Everton J. Carpes - em 19/03/2009 03:18
Blog: MyWay

Karmômetro (?)

tende a neutro
adicionar comentário Comment

Refactoring

Um dos princípios básicos para mantermos uma boa codificação é evitarmos repetições, >DRY .

Refatorar é uma das mais importantes práticas no desenvolvimento ágil de software, e deveria ser abraçada por qualquer programador interessado em conservar uma boa qualidade de código e evoluir sua capacidade técnica. Ao revisarmos nossos códigos, aprendemos a “olhar a mesma coisa com outros olhos”, a separar melhor conceitos, evitar mais o acoplamento entre partes de nosso código. Além de servir para manter a Q.A., refatorar é um habito que rapidamente se torna um vicio bom, a favor da sanidade do programador e da qualidade do serviço oferecido.

Apesar de todas suas vantagens, refatorar não é uma tarefa simples, uma vez que você não pode simplesmente sair abrindo cada arquivo em sua aplicação, procurando aleatoriamente por similaridades ou práticas ruins.

Obviamente existem ferramentas e técnicas para se encontrar similaridades estruturais em códigos, de forma a detectar possíveis candidatos. Isomorfismos em códigos, seja em paradigmas Orientados a Objetos ou simplesmente funcionais, normalmente indicam uma modularização ruim de algum comportamento.

Muitas vezes, nos vemos “copiando e colando” código entre regiões, arquivos e camadas de nossas aplicações, replicando comportamentos que deveriam obviamente serem encapsulados em alguma estrutura que nos permita compartilhar este comportamento entre um ou mais objetos (funções, palavras, ou seja la o que for).

Flay

Em Ruby, temos uma excelente ferramenta denominada Flay, encapsulada em uma Gem que possui um simples executável, responsável por criar um relatório que aponta possíveis similaridades.

Instalação

Para instalar o Flay basta instalar a gem com o mesmo nome:

$ gem install flay

Uso

Para utiliza-lo, basta por exemplo, invocar o Flay, com o path para o que pretendemos comparar:

$ flay app/models/

Exemplos reais de Refactoring guiado pelo Flay

Vou exemplificar aqui um pequeno refactoring nos modelos do site da Geek .

Executar o Flay nos retorna um relatório que da uma pontuação a nosso código, que como ele mesmo salienta no relatório, quanto menor for a pontuação que recebermos, menos similaridades ele encontrou (e conseqüentemente melhor deve estar a qualidade de nosso código).

Relatório basico

O relatório básico indica o score e consta com uma lista de cada arquivo onde a similaridade foi detectada, qual seu peso (quanto o Flay considera grave o problema) e em que linhas se iniciam as similaridades.

$ flay app/models
Processing app/models/user_observer.rb
...
Processing app/models/search_observer.rb
Total score (lower is better) = 322


1) Similar code found in :iter (mass = 92)
  app/models/affiliation_state_machine.rb:28
  app/models/affiliation_state_machine.rb:40

...

3) Similar code found in :defn (mass = 42)
  app/models/comment.rb:59
  app/models/asset.rb:33

...

Relatório avançado (verbose)

Um relatório mais detalhado pode ser obtido ao passar o parâmetro -v (verbose), que retorna um relatório indicando as similaridades, linha a linha.

$ flay -v app/models
Processing app/models/user_observer.rb
...
Processing app/models/search_observer.rb
Total score (lower is better) = 322


1) Similar code found in :iter (mass = 92)
  A: app/models/affiliation_state_machine.rb:28
  B: app/models/affiliation_state_machine.rb:40

A: event(:deny) do
B: event(:delete) do
A:   transitions(:from => [:pending, :approved, :denied], :to => :denied, :guard => Proc.new do |aff|
B:   transitions(:from => [:pending, :approved, :denied], :to => :deleted, :guard => Proc.new do |aff|
       unless blank = (not aff.comment.blank?) then
         msg = ::ActiveRecord::Errors.default_error_messages[:blank]
         aff.errors.add(:comment, msg)
       end
       blank
     end)
   end

...

3) Similar code found in :defn (mass = 42)
  A: app/models/comment.rb:59
  B: app/models/asset.rb:33

A: def user_url=(url)
B: def video_url=(url)
     return if url.blank?
     url = "http://#{url}" unless url =~ /^(.*)\:\/\//i
A:   write_attribute(:user_url, url)
B:   write_attribute(:video_url, url)
   end


...

Os relatórios podem conter similaridades referentes a apenas arquivo ou similaridades cruzadas entre arquivos.

Primeiro caso: similaridades no mesmo arquivo

Citando o exemplo da similaridade encontrada no arquivo app/models/affiliation_state_machine.rb



1) Similar code found in :iter (mass = 92)
  A: app/models/affiliation_state_machine.rb:28
  B: app/models/affiliation_state_machine.rb:40

A: event(:deny) do
B: event(:delete) do
A:   transitions(:from => [:pending, :approved, :denied], :to => :denied, :guard => Proc.new do |aff|
B:   transitions(:from => [:pending, :approved, :denied], :to => :deleted, :guard => Proc.new do |aff|
       unless blank = (not aff.comment.blank?) then
         msg = ::ActiveRecord::Errors.default_error_messages[:blank]
         aff.errors.add(:comment, msg)
       end
       blank
     end)
   end


É obvio neste exemplo a similaridade, o Proc de validação de uma Afiliação (que neste caso, é o modelo “join”, que conecta informações entre Blogs e Colletivos), é absolutamente o mesmo entre os processos de negar ou deletar um pedido. No caso, um pedido de Afiliação de um Blog nesse sistema, é feito pelo usuário dono do Blog e aguarda uma autorização (do dono do Coletivo)… Para que o pedido possa ser negado ou ignorado, o dono do Coletivo precisa informar ao Blogueiro a causa do pedido não ter sido aceito, e portanto o comment não pode estar em branco.

A mudança em questão foi super simples, bastou criar apenas um Proc e compartilhá-lo entre os eventos. Como os eventos são absolutamente iguais estruturalmente, eu aproveitei a alteração e usei apenas um iterator para definir os estados:

  detect_blank = Proc.new do |aff|
    unless blank = !aff.comment.blank? 
      msg = ::ActiveRecord::Errors.default_error_messages[:blank]
      aff.errors.add(:comment, msg)
    end
        
    blank 
  end
      
  { :deny => :denied, :delete => :deleted }.each do |e, state|
    event e do
    transitions(:from => [:pending, :approved, :denied], 
                :to   => state,  :guard => detect_blank)
    end
  end

Obs.: Sim, eu prefiro separar a lógica referente as máquinas de estado de determinados modelos, em arquivos próprios, englobados em modules que eu incluo no modelo, de forma a manter mais legível e isolado cada porcão do código.

Segundo caso: similaridades em múltiplos arquivos

Quanto ao exemplo das similaridades encontradas nos múltiplos arquivos (app/models/comment.rb e app/models/asset.rb), podemos perceber outro tipo de situação:


3) Similar code found in :defn (mass = 42)
  A: app/models/comment.rb:59
  B: app/models/asset.rb:33

A: def user_url=(url)
B: def video_url=(url)
     return if url.blank?
     url = "http://#{url}" unless url =~ /^(.*)\:\/\//i
A:   write_attribute(:user_url, url)
B:   write_attribute(:video_url, url)
   end


O exemplo também é simples, porém ilustra muito bem um problema que comum: compartilhar comportamentos entre estruturas. No caso em questão, ambos os modelos (Comment e Asset possuem campos distintos, que deve conter uma URL). Em ambos os casos, eu preciso que por padrão o protocolo HTTP preceda a URL. A codificação é simples, mas copiar e colar o bloco de código alterando o nome do método (como eu havia feito originalmente :p ), não é uma boa pratica. Se eu precisar alterar este comportamento em um lugar, posso acabar me esquecendo de alterar no outro.

Citei justamente este caso, porque ele exemplifica uma situação onde o comportamento não apenas esta compartilhado entre modelos de uma mesma aplicação, mas certamente eu precisarei replicar o mesmíssimo comportamento em outras aplicações. Este seria o momento de cogitar incluir isto em um plugin. Dada a simplicidade da questão, eu preferi não isolar isso em um plugin completo, mas incorporá-lo em um plugin que uso justamente com comportamentos que altero do Rails e pequenas funcionalidades que desejo compartilhar entre aplicações. Não cabe aqui explanar sobre como criar um plugin pro rails , mas essencialmente através de um plugin bem simples, eu adicionei esta feature a ActiveRecord::Base chamando o método utilitário de acts_as_url, o qual recebe um parâmetro que eh o nome do campo (por default, :url).

class ActiveRecord::Base
  def self.acts_as_url(attr = :url)
    return unless attr
    
    define_method("#{attr}=") do |url|
      return if url.blank? 
    
      url = "http://#{url}" unless url =~ /^(.*)\:\/\//i
      write_attribute(attr, url)
    end
  end
end

Obs.: Não, você não precisa criar um plugin para compartilhar novos códigos entre suas aplicações, especialmente para algo tao simples. Eu fiz isso, apenas porque já tinha esta facilidade, além do que, eu necessito compartilhar macros Shoulda para testar se esta funcionalidade esta operando corretamente. Uma sugestão para quem não pretende compartilhar nada a mais, é simplesmente adicionar este comportamento diretamente na ActiveRecord, no processo de inicialização (no environment.rb ou mais apropriadamente em algum initializer).

Um micro-exemplo: usando o _with_options_

Uma similaridade simplíssima apontada pelo Flay no arquivo do modelo de posts, foi um local onde eu tinha uma chamada em seqüencia a um método (no caso, um método de transcrição de encodings, referente ao processo que envia noticias para o Yahoo News):


5) Similar code found in :iter (mass = 38)
  A: app/models/post.rb:83
  B: app/models/post.rb:84

A: article.headline do |h|
B: article.summary do |s|
A:   (h << iconv(self.title, :encoding => options[:encoding]))
B:   (s << iconv(self.summary, :encoding => options[:encoding]))
   end

Assim como neste exemplo, é super comum chamarmos vários métodos com um ou mais parâmetros absolutamente iguais, mas o Rails nos oferece uma ferramenta muitíssimo útil para eliminarmos esta repetição, o método with_options. O exemplo refatorado para usar este método fica assim:

with_options(:encoding => options[:encoding]) do
  article.headline {|h| h << iconv(self.title)   }
  article.summary  {|s| s << iconv(self.summary) }
  article.story    {|s| s << iconv(story)        }
end

Obs.: Era possível ainda iterar sobre uma Hash com o valor e o nome do campo, mas eu preferi manter assim porque considerei mais legível.

Concluindo

Ainda que o Flay seja apenas um guia (Assim como o RCOV para testes), ela eh de grande valia. Uma das questões mais importantes é mantermos sempre vivo o desejo de atingir melhorias na qualidade da nossa codificação e um grande passo é conseguirmos eliminar estas pequenas repetições que no dia a dia acabamos inserindo em nosso código.

Os casos aqui citados são apenas melhorias na codificação da aplicação, porem a mesmíssima logica deve ser aplicada por exemplo aos testes e a tudo que fazemos. Sempre devemos buscar o principio de não nos repetir.


Tags:

Postado por Everton J. Carpes - - em 19/03/2009 03:18

Comentários Comment

  1. coment&aacute;rio de Vinícius Alves Hax

    Karmômetro (?)

    tende a neutro

    Uma bela dica, não conhecia.

    Vai ser bem útil.

    Abraços.

    Postado por Vinícius Alves Hax em 19/03/2009 09:17

  2. coment&aacute;rio de Everton J. Carpes

    Karmômetro (?)

    tende a neutro

    @Vinícius Alves Hax

    O Flay eh uma das ferramentas que eu venho adotando a um bom tempo, com intuito de melhorar a qualidade da minha codificação. Existem outras 2 ferramentas “irmãs” dele, que são igualmente úteis e sobre as quais pretendo postar em breve:

    1. Flog – uma ferramenta para medição de Complexidade Ciclomática
    2. Heckle – uma poderosa ferramenta que testa a cobertura de teus testes alterando teu código (dinamicamente) e analisando se teus testes perceberam a diferença. Heckle eh provavelmente a chave para uma busca de uma cobertura de testes mais sólida.

    Podes ler sobre as 3 ferramentas no site Confessions of a Ruby Sadist

    Postado por Everton J. Carpes - em 19/03/2009 19:00

Postar um novo comentário

Não preencha este campo Ele é um mecanismo para evitarmos spams. Se vc. está vendo este texto, seu browser provavelmente não interpreta corretamente CSS. De qualquer forma, apenas deixe este campo em branco e siga livre para comentar.

Ajuda com a formatação


voltar ao início