Fernando Luizão

Desenvolvimento de software e nerdices em geral

Posts Tagged ‘Ruby

Tradução: Delegação em Ruby

with 4 comments

Hoje li um artigo interessante na primeira edição da Rails Magazine sobre delegação de métodos em Ruby, escrito por Khaled al Habache. Além da delegação, ele também aborda um tema antigo (e que gera muita discussão por aí) em se tratando de POO, Herança X Composição. Como não achei material sobre delegação em português, resolvi traduzir o artigo. Espero que gostem :).

Delegação em Ruby

“Separar partes mutáveis das partes que não mudam” e “composição é preferível a herança” são dois princípios de projeto comuns quando você inicia no mundo da Programação Orientada à Objetos. Entretanto, enquanto o primeiro parece lógico, alguém pode pensar porque é preferível usar composição em vez de herança, e é uma questão lógica, então vamos respondê-la com um exemplo.

Vamos supor que temos um robô que possui um sensor de temperatura, representado pelo seguinte diagrama UML:

Robô

Classe Robô

Esse projeto tem alguns inconvenientes:

1. Existe grande probabilidade de existir outros tipos de robôs que não possuem sensores de calor (quebra o primeiro princípio de projeto: separar código mutável do estático)
2. Sempre que eu desejar alterar qualquer coisa relacionada ao sensor de temperatura, terei que alterar a classe Robô (quebra o primeiro princípio de projeto).
3. Exposição dos métodos do sensor de temperatura na classe Robô.

Vamos melhorar essa classe um pouco:

Classe Robô Vulcão

Classe Robô Vulcão

Bem, agora esse é um projeto baseado em herança que resolve o primeiro problema, mas ainda é incapaz de resolver os outros dois problemas relacionados ao sensor de calor. Vamos melhorar um pouco mais:

Delegação

Delegação

Esse é um típico modelo, baseado em composição em vez de herança. Com ele podemos resolver todos os 3 problemas, e ainda por cima ganhamos uma nova classe: agora podemos abstrair o SensorDeTemperatura para usos futuros.

O que é delegação?

Delegação é o processo de delegar funcionalidade às partes contidas.

Se você olhar cuidadosamente à figura anterior, você notará que o RoboVulcao ainda possui 3 métodos relacionados ao sensor; esses são métodos que apenas chamam os métodos correspondentes do sensor. Isso é exatamente o que delegação é, apenas repassar funcionalidade às partes contidas.

Delegação vem junto com composição para oferecer soluções flexíveis e elegantes como essa que vimos anteriormente, e também respeita o princípio “separar código mutável de código estático”, mas também cobra um preço por isso: a necessidade de métodos que “embrulhem” as chamadas impõem um tempo extra de processamento por causa das chamadas a esses métodos.

Ruby e delegação

Agora vamos ver um exemplo de código:

Temos um robô de propósito geral que possui um Braço e um SensorDeCalor. O robô é capaz de executar várias tarefas, como empacotar caixas, empilhá-las e medir a temperatura.

Usaremos composição e delegação assim:

class Robô

  def initialize
    @sensor_temperatura = SensorDeTemperatura.new
    @braco = BracoDoRobo.new
  end

  def medir_temperatura(escala="c")
    @sensor_temperatura.measure(escala)
  end

  def empilhar(quantidade_de_caixas=1)
    @braco.empilhar(quantidade_de_caixas)
  end

  def empacotar
    @braco.empacotar
  end

end

class SensorDeTemperatura

  # Escala Celsius ou Fahrenheit
  def medir(escala="c")
    t = rand(100)
    t = escala=="c" ? t : t * (9/5)
    puts "A temperatura é #{t}° #{escala.upcase}"
  end

end

class BracoDoRobo

  def empilhar(quantidade_de_caixas=1)
    puts "Empilhando #{quantidade_de_caixas} caixa(s)"
  end

  def empacotar
    puts "Empacotando"
  end

end

robo = Robo.new #=>#<Robo:0xb75131e8 @arm=#<BracoDoRobo:0xb75131ac>, @sensor_temperatura=#<SensorDeTemperatura:0xb75131c0>>
robo.empilhar 2 #=>Empilhando 2 caixa(s)
robo.empacotar #=>Empacotando
robo.medir_temperatura #=> A temperatura é 59° C

Como pode ser visto, temos 3 métodos que “encapsulam” chamadas (empilhar, empacotar e medir_temperatura) na classe Robo que não fazem nada a não ser chamar o método correspondente dos componentes (BracoDoRobo e SensorDeTemperatura).

Isso é uma coisa péssima, especialmente quando existem vários objetos contidos no objeto principal.

Entretanto, em Ruby temos duas bibliotecas para nos salvar: Forwardable e Delegate, Vamos ver cada uma delas.

Biblioteca Forwardable

A biblioteca Forwardable possui dois módulos, Forwardable e SingleForwardable.

Módulo Forwardable

O módulo Forwardable oferece delegação dos métodos especificados para um objeto designado, usando os métodos def_delegator e def_delegators.

def_delegator(obj, method, alias = method): Define um método “method” que delega a chamada ao objeto “obj”. Se o alias for fornecido, será usado como o nome do método de delegação.

def_delegators(obj, *methods): Atalho para definir múltiplos métodos delegadores, mas sem permitir o uso de um nome diferente.

Vamos refatorar nosso exemplo de robô para utilizar o módulo Forwardable:

require 'forwardable'

class Robo

  # Extend fornece métodos de classe
  extend Forwardable

  # Uso do  def_delegators
  def_delegators :@braco, :empacotar, :empilhar

  # Uso do  def_delegator
  def_delegator :@sensor_temperatura, :measure, :medir_temperatura

  def initialize
    @sensor_temperatura = SensorDeTemperatura.new
    @braco = BracoDoRobo.new
  end

end

class SensorDeTemperatura

  # Escala Celsius ou Fahrenheit
  def medir(escala="c")
    t = rand(100)
    t = escala=="c" ? t : t * (9/5)
    puts "A temperatura é #{t}° #{escala.upcase}"
  end

end

class BracoDoRobo

  def empilhar(quantidade_de_caixas=1)
    puts "Empilhando #{quantidade_de_caixas} caixa(s)"
  end

  def empacotar
    puts "Empacotando"
  end

end

Como pode ser visto, é uma solução mais limpa e consisa.

Módulo SingleForwardable

O módulo SingleForwardable oferece delegação dos métodos especificados para um objeto designado, utilizando os métodos def_delegator e def_delegators. Esse módulo é similar ao Forwardable, mas trabalha com os próprios objetos, em vez de suas classes que os definem.

require "forwardable"
require "date"

date = Date.today #=> #<Date: 4909665/2,0,2299161>
# Prepara o objeto para delegação
date.extend SingleForwardable #=> #<Date: 4909665/2,0,2299161>
# Adiciona delegação para Time.now
date.def_delegator :Time, "now","with_time"
puts date.with_time #=>Thu Jan 01 23:03:04 +0200 2009

Biblioteca Delegate

Delegate é outra biblioteca que oferece delegação, irei explicar duas formas de usá-la.

Método DelegateClass

Utiliza o método de nível superior DelegateClass, que permite inicializar a delegação por meio de herança. No exemplo seguinte, quero criar uma nova classe chamada CurrentDate, que armazene a data corrente e alguns métodos extras, ao mesmo em que delego a objetos data normais:

require "delegate"
require "date"

# Note a definição da classe
class CurrentDate < DelegateClass(Date)

  def initialize
    @date = Date.today
    # Passa o objeto a ser delegado à superclasse
    super(@date)
  end

  def to_s
    @date.strftime "%Y/%m/%d"
  end

  def with_time
    Time.now
  end

end

cdate = CurrentDate.new
# Note como funciona a delegação
# Em vez de usar cdate.date.day e definir um
# attr_accessor para date, eu uso c.day
puts cdate.day #=>1
puts cdate.month #=>1
puts cdate.year #=>2009
# Testando métodos adicionados
# to_s
puts cdate #=> 2009/01/01
puts cdate.with_time #=> Thu Jan 01 23:22:20 +0200 2009

Classe SimpleDelegator

Use-a para delegar para um objeto que mode ser modificado:

require "delegate"
require "date"

today = Date.today #=> #<Date: 4909665/2,0,2299161>
yesterday = today - 1 #=> #<Date: 4909663/2,0,2299161>
date = SimpleDelegator.new(today) #=> #<Date: 4909665/2,0,2299161>
puts date #=>2009-01-01
# Usa __setobj__ para trocar a delegação
date.__setobj__(yesterday)#=> #<Date: 4909663/2,0,2299161>
puts date #=>2008-12-31

Como pode ser visto, criamos dois objetos e delegamos a eles.

E o Rails?

Rails adiciona uma nova funcionalidade chamada “delegate”, que fornece um método de classe para expor facilmente os metodos dos objetos contidos como se fossem métodos próprios. Passe um ou mais métodos (especificados como símbolos ou strings) e como último parâmetro o nome do objeto alvo na opção :to (também como símbolo ou string). Pelo menos um método e a opção :to são exigidos.

Vá para o console, crie um novo projeto e inicie um console do rails:

$ rails dummy
$ cd dummy
$ruby script/console
Loading development environment (Rails 2.2.2)
>> Person = Struct.new(:name, :address)
=> Person
>> class Invoice < Struct.new(:client)
>>   delegate :name, :address, :to => :client
>> end
=> [:name, :address]
>> john_doe = Person.new("John Doe", "Vimmersvej 13")
=> #<struct Person name="John Doe", address="Vimmersvej 13">
>> invoice = Invoice.new(john_doe)
=> #<struct Invoice client=#<struct Person name="John Doe", address="Vimmersvej 13">>
>> invoice.name
=> John Doe
>> invoice.address
=>Vimmersvej 13

Recomendo fortemente que você verifique todos os exemplos na documentação da API do Rails, para ver como usar esse recurso efetivamente com o ActiveRecord.

Antes de terminar esse artigo, quero compartilhar com você o código do método delegate do Rails. Adicionarei alguns comentários para explicar o que está acontecendo:

class Module

  # método delegate
  # Espera um array de argumentos contendo os métodos
  # a serem delegados e um hash de opções
  def delegate(*methods)
    # Desempilha o hash de opções
    options = methods.pop
    # Verifica se o hash de opções foi passado, e se contém a opção :to
    # Lança uma exceção se um dos dois não forem encontrados
    unless options.is_a?(Hash) && to = options[:to]
      raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter)."
    end

    # Certifica-se de que a opção :to segue algumas
    # regras de sintaxe para nomes de métodos
    if options[:prefix] == true && options[:to].to_s =~ /^[^a-z_]/
      raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
    end

    # Atribui o verdadeiro valor do prefixo
    prefix = options[:prefix] && "#{options[:prefix] == true ? to : options[:prefix]}_"

   # Aqui vem a mágica do Ruby 🙂
   # Técnicas de reflexão são usadas aqui:
   # module_eval é usado para adicionar novos métodos em tempo de execução, onde:
   # expõe os métodos dos objetos contidos
    methods.each do |method|
      module_eval("def #{prefix}#{method}(*args, &block)\n#{to}.__send__(#{method.inspect}, *args, &block)\nend\n", "(__DELEGATION__)", 1)
    end

  end

end

Isso é tudo para esse artigo, foram cobertos 5 pontos:

1. Composição versus Herança.
2. O que é delegação, e porque ela é usada.
3. Biblioteca Forwardable.
4. Biblioteca Delegate.
5. Método delegate do Rails.

Advertisements

Written by fernandoluizao

March 31, 2009 at 1:48 am