Ruby – DRY, Refactoring e Otimização

June 8th, 2008 Enviado em Ruby

Ruby – DRY, Refactoring e Otimização

Este artigo é todo baseado no 2º exercício da 5ª aula de um curso online de Ruby (http://rubylearning.org/) que estou fazendo (junho de 2008), turma FORPC101-5C, na verdade baseado na minha solução para o exercício, se você é ou foi aluno deste curso pode acompanhar a thread desta solução no fórum do curso, neste link http://rubylearning.org/class/mod/forum/discuss.php?d=575&parent=9715

Como ja expus aqui, considero este curso surpreendente, nunca é demais repetir, jamais pensei que um dia elogiaria um curso online, mas realmente este é de tirar o chapéu, para quem, como eu, gosta do tipo de metodologia em que se valoriza a participação e a prática tanto quanto a teoria, acredito que este curso se encaixa como uma luva. Bem, chega de rasgação de seda e vamos a mais uma ponderação sobre alguns tópicos de Ruby. Nesta 5ª semana do curso (a duração é de 8 semanas) aprendi muito com muito pouco, eu explico, com um problema ridículo pude aprender sobre refactoring, otimização e o conceito DRY em Ruby. Dito isto vamos ao “game”: O 2º exercício da 5ª semana é o seguinte:

Escreva a classe Rectangle. O uso da mesma deve ser feito da seguinte forma:

r =  Rectangle.new(23.45, 34.67)
puts 'Area is = ' + r.area().to_s
puts 'Perimeter  is = ' + r.perimeter.to_s

Ou seja, criar uma instancia da classe e apresentar na tela a área e o perímetro do retângulo. A solução, digamos… óbvia, é a que a todos imediatamente pensaram: Escrever uma classe que receba 2 parâmetros em seu método de inicialização, crie 2 variáveis de instancia com estes parâmetros, esta classe possuiria 2 métodos, um para calcular área e outro para calcular o perímetro. Todos tiveram a mesma idéia e produziram praticamente o mesmo código, comigo não foi diferente, sabendo que a área de um triangulo é base*altura e que o perímetro é 2*(base+altura), produzi o seguinte código:
class Rectangle
  def initialize(b, h)
   @base = b.to_f
   @height = h.to_f
  end

  def area
   return @base*@height
  end

  def perimeter
    return 2*@base+2*@height
  end
end

r = Rectangle.new(23.45, 34.67)
puts 'Area is = ' + r.area().to_s
puts 'Perimeter is = ' + r.perimeter.to_s

Iniciantes:
1 – Initialize é o método ‘construtor’ das classes em Ruby
2 – A palavra chave def define um método
3 – Em uma classe um nome de variável precedida pelo sinal de arroba (@) indica que é uma variável de instância.
4 – puts apresenta uma string na tela

Ok, foi a solução encontrada por todos, mas no fórum, no thread sobre o exercício, um dos membros dos professores levantou as seguintes questões

“O grupo deveria considerar algumas coisas não demonstradas nestas soluções. Após a instancia da classe Rectangle ser criada – e a menos que o triangulo seja “redimensionado” – a área e o perímetro sempre serão os mesmos. Na verdade são como propriedades da instancia do retângulo. Assim sendo, porque recalcular a área e o perímetro todas as vezes em que é preciso obte-los? … “http://rubylearning.org/class/mod/forum/discuss.php?d=575&parent=9451 “… ”

“… Mas o que é o estado de um retângulo? Anteriormente vocês consideraram o estado do retângulo como sendo a base e altura, agora vocês estão considerando a área e o perímetro. Mas o que define um retângulo?..” http://rubylearning.org/class/mod/forum/discuss.php?d=575&parent=9463

Tornando mais OO
Estas questões intrigaram os participantes e surgiram várias piadinhas, eheheh. Bem, acabei atentando para a observação das execuções repetidas dos métodos e resolvi fazer uma classe onde o usuário da mesma pudesse ter acesso a área e ao perímetro através de variáveis de instancia, desta maneira teríamos apenas um método para os cálculos que seria chamado apenas uma vez, internamente pela classe, mas com meu parco conhecimento de ruby como fazer para que o usuário tenha acesso às variáveis de instancia? Para quem já programava em outra linguagem OO isto é muito simples, basta pensar da mesma forma, em Java ou em Delphi bastaria criar um método para acessar estas variáveis, o que se convencionou chamar em Java de Getters and Setters methods, mas por via das dúvidas resolvi consultar a documentação (fantástico a quantidade de docs disponíveis) e acabei descobrindo que em Ruby isto é ainda mais simples, podemos criar variáveis de instancia como sendo atributos acessíveis através do atalho attr_reader, quando pesquisava a documentação descobri também que para que um método não seja acessível fora da classe basta colocar a palavra private antes da definição do método, desta maneira todos os métodos definidos em seguida serão privados até que se encontre a palavra public, minha nova versão do código ficou assim:

class Rectangle
  attr_reader :area, :perimeter
  def initialize(b, h)
    @area, @perimeter = 0.0, 0.0
    calcs(b.to_f, h.to_f)
  end 

  private
  def calcs(b,h)
    @area = b*h
    @perimeter = 2*(b+h)
  end
end

r = Rectangle.new(23.45, 34.67)
puts r.area
puts r.perimeter

Iniciantes:

Pode-se fazer atribuições paralelas em ruby, é o que faço na primeira linha do método initialize, exemplo: a, b = 1,2 seria o mesmo que a atribuição em 2 linhas, assim:

a = 1

b = 2

Nada mal, agora economizamos um método, mas olhando atentamente percebi que dava para melhorar ainda mais, para que o método calcs? Porque não fazer todos os cálculos no método initialize e ainda deixar o usuário ter acesso as variáveis base e altura do triangulo? Então criei o código abaixo e o postei no fórum:

class Rectangle
  attr_reader :area, :perimeter, :height, :base
  def initialize(b, h)
    @base = b.to_f
    @height = h.to_f
    @area = @base*@height
    @perimeter = 2*(@base+@height)
  end
end
r = Rectangle.new(23.45, 34.67)
puts "Rectangle Properties:\n"
puts 'Base: ' + r.base.to_s
puts 'Height: ' + r.height.to_s
puts 'Area: ' + r.area.to_s
puts 'Perimeter: ' + r.perimeter.to_s

Refatorando e melhorando
Agora parece que ficou mais “cool”, quando a instância da classe é criada e o método initialize (toda classe em ruby dever ter este método que funciona como método construtor da classe) é executado, todos os cálculos necessários são feitos e o usuário da classe terá pleno acesso a todas as “propriedades” ou o “estado” do triangulo através das variáveis de instancia. Mas olhando o código no fórum (http://rubylearning.org/class/mod/forum/discuss.php?d=575&parent=9715), fiquei pensando que desta forma a classe estava muito engessada, não havia possibilidade do usuário redimensionar o triangulo foi então que decidi refatorar mais uma vez adicionando um método chamado resize, vamos ao código:
class Rectangle
  attr_reader :area, :perimeter, :height, :base
  def initialize(b, h)
    @base = b.to_f
    @height = h.to_f
    calcs
  end
  def resize(b, h)
    @base = b.to_f
    @height = h.to_f
    calcs
  end
  private
  def calcs
    @area = @base*@height
    @perimeter = 2*(@base+@height)
  end
end

Aplicando o conceito DRY
Legal, agora se pode ter acesso a todas as propriedades do triangulo e ainda redimensioná-lo, o código parece perfeito, mas olhando atentamente o mesmo fere um conceito muito caro aos gurus do Ruby, este conceito chama-se DRY (Don’t Repeat Yourself), veja que há duas linhas que se repetem na classe, as duas primeiras do método initialize são repetidas no inicio do método resize, calcs é chamado no método initialize e também no método privado calcs, parece bem óbvio depois que alguém aponta, mas quando estamos programando parece que o afã de ver o resultado final bloqueia nossa visão para estes detalhes, se não fosse o conceito DRY martelando no meu cérebro, com certeza eu não atentaria para isto. Bem, depois destas observações arregacei as mangas e produzi o código final:
class Rectangle
  attr_reader :area, :perimeter, :height, :base
  def initialize(b, h)
    resize(b,h)
  end

  def resize(b, h)
    @base = b.to_f
    @height = h.to_f
    calcs
  end

  private
  def calcs
    @area = @base*@height
    @perimeter = 2*(@base+@height)
  end
end

r = Rectangle.new(23.45, 34.67)
puts "Rectangle Properties:\n"
puts 'Base: ' + r.base.to_s
puts 'Height: ' + r.height.to_s
puts 'Area: ' + r.area.to_s
puts 'Perimeter: ' + r.perimeter.to_s
puts "*" * 30
r.resize(53.45, 44.67)
puts "New Rectangle Properties:\n"
puts 'Base: ' + r.base.to_s
puts 'Height: ' + r.height.to_s
puts 'Area: ' + r.area.to_s
puts 'Perimeter: ' + r.perimeter.to_s
puts "*" * 30

Veja que agora as variáveis de instância são criadas no método resize e não mais no initialize, para isto chamo resize no initialize, como o método resize chama calcs, mais uma vez evito a repetição. Desta forma esta classe fica praticamente perfeita.

Mais refactoring
Eu disse perfeita? não! praticamente, né? depois que achava que já tinha acabado este artigo, voltei ao forum e tive a grata surpresa de um novo refactoring de um dos participantes -Jeff Hales- Gente finissima, em sua explanação ele explica que não há razão para o métdo calcs, veja o que ele fez com a classe, realmente um refactoring matador:

<span style="font-weight: bold; color: #9966cc;">class</span> Rectangle
  attr_reader :area, :perimeter, :height, :base
  <span style="font-weight: bold; color: #9966cc;">def</span> initialize<span style="font-weight: bold; color: #006600;">(</span>b, h<span style="font-weight: bold; color: #006600;">)</span>
    size<span style="font-weight: bold; color: #006600;">(</span>b,h<span style="font-weight: bold; color: #006600;">)</span>
  <span style="font-weight: bold; color: #9966cc;">end</span>

  <span style="font-weight: bold; color: #9966cc;">def</span> size<span style="font-weight: bold; color: #006600;">(</span>b,h<span style="font-weight: bold; color: #006600;">)</span>
    @base = b.<span style="color: #9900cc;">to_f</span>
    @height = h.<span style="color: #9900cc;">to_f</span>
    @area = @base*@height
    @perimeter = <span style="color: #006666;">2</span>*<span style="font-weight: bold; color: #006600;">(</span>@base+@height<span style="font-weight: bold; color: #006600;">)</span>
  <span style="font-weight: bold; color: #9966cc;">end</span>

  <span style="font-weight: bold; color: #9966cc;">alias</span> resize size
<span style="font-weight: bold; color: #9966cc;">end</span>

O único “porém” neste refactoring apontado pelo professor José Carlos Monteiro e que não tem como não concordar é que este aliás é absoultamente desnecessário, além do ‘drop’ deste aliás será que dá para fazer mais alguma coisa para melhorar esta classe? quem se habilita?

Tags:

One Response par “Ruby – DRY, Refactoring e Otimização”

  1. Helder Informa:

    Muito bom!!

    PS.: Triângulo Retângulo rsrsrsr



Deixe um comentário