Arquivo

Textos com Etiquetas ‘Construtores’

Múltiplos construtores em Ruby (minha versão)

10 de setembro de 2010

Ontem, através de uma mensagem no Twitter do André Moreira, estava lendo um post no blog do Rinaldi Fonseca falando sobre múltiplos construtores em Ruby. Comecei a escrever um comentário, que acabou se tornando muito grande. Então achei melhor escrever aqui para expressar minha opinião a respeito.

Na primeira parte do post é mostrado a utilização de métodos de classe para construir um novo objeto passando parâmetros diferentes dos recebidos no construtor, onde foi usado esse exemplo:

class Carro
  attr_accessor :marca, :placa, :dono

  def initialize(marca, placa, dono)
    @marca, @placa, @dono =  marca, placa, dono
  end

  def Carro.carro_sem_dono(marca, placa)
    new marca, placa, "SEM DONO"
  end

  def Carro.carro_sem_placa(marca, dono)
    new marca, "SEM PLACA", dono
  end
end

carro = Carro.new "Ferrari", "ABC1234", "João"
carro_sem_dono  = Carro.carro_sem_dono "Vectra", "ABC5678"
carro_sem_placa = Carro.carro_sem_placa "Palio", "José"

puts carro.inspect           #<Carro: @dono="João", @placa="ABC1234", @marca="Ferrari">
puts carro_sem_dono.inspect  #<Carro: @dono="SEM DONO", @placa="ABC5678", @marca="Vectra">
puts carro_sem_placa.inspect #<Carro: @dono="José", @placa="SEM PLACA", @marca="Palio">

Os métodos carro_sem_dono e carro_sem_placa seguem o Factory Method Design Pattern (Padrão de Projeto Método Fábrica) e atuam como uma DSL na classe Carro. Desse modo, não há necessidade da redundância do nome da classe no ínicio de cada método fábrica:

class Carro
  attr_accessor :marca, :placa, :dono

  def initialize(marca, placa, dono)
    @marca, @placa, @dono =  marca, placa, dono
  end

  def self.sem_dono(marca, placa)
    new marca, placa, "SEM DONO"
  end

  def self.sem_placa(marca, dono)
    new marca, "SEM PLACA", dono
  end
end

carro = Carro.new "Ferrari", "ABC1234", "João"
carro_sem_dono  = Carro.sem_dono "Vectra", "ABC5678"
carro_sem_placa = Carro.sem_placa "Palio", "José"

puts carro.inspect           #<Carro: @dono="João", @placa="ABC1234", @marca="Ferrari">
puts carro_sem_dono.inspect  #<Carro: @dono="SEM DONO", @placa="ABC5678", @marca="Vectra">
puts carro_sem_placa.inspect #<Carro: @dono="José", @placa="SEM PLACA", @marca="Palio">

Já na segunda parte do post é mostrada uma maneira de se passar um bloco para o construtor da classe Carro e assim inicializar seus atributos:

class Carro
  attr_accessor :ano, :marca, :modelo, :dono, :cor, :tipo

  def initialize(&block)
    instance_eval &block
  end
end

carro = Carro.new do
  self.ano    = "2000"
  self.marca  = "Gol"
  self.modelo = "Exemplo"
  self.dono   = "Dono exemplo"
  self.cor    = "Vermelho"
  self.tipo   = "Tipo exemplo"
end

puts carro.inspect #<Carro: @ano="2000", @marca="Gol", @modelo="Exemplo", @dono="Dono exemplo", @cor="Vermelho", @tipo="Tipo exemplo">

Eu particularmente gosto do tipo de inicialização de atributos de um novo objeto permitada nas classes da camada Model de Ruby on Rails. O método new dessas classes (que herdam de ActiveRecord::Base) podem receber tanto um hash quanto um bloco com os valores dos atributos. Dessa maneira, a classe Carro poderia ser inicializada assim:

carro = Carro.new :ano => "2000",
                   :marca => "Gol",
                   :modelo => "Exemplo",
                   :dono => "Dono exemplo",
                   :cor => "Vermelho",
                   :tipo => "Tipo exemplo"

#ArgumentError: wrong number of arguments (1 for 0)

Eu disse poderia. Não pode, pois o construtor new espera um bloco e não um hash, e Carro não é uma classe Model do Rails.

Para solucionar isso, podemos modificar o construtor da classe Carro para receber um hash de atributos ao invés de um bloco. Então atribuímos cada valor presente no hash para seu respectivo atributo:

class Carro
  attr_accessor :ano, :marca, :modelo, :dono, :cor, :tipo

  def initialize(attributes = nil)
    attributes.each do |attr, value|
      self.send("#{attr}=", value)
    end unless attributes.nil?
  end
end

carro = Carro.new :ano => "2000",
                   :marca => "Gol",
                   :modelo => "Exemplo",
                   :dono => "Dono exemplo",
                   :cor => "Vermelho",
                   :tipo => "Tipo exemplo"

puts carro.inspect #<Carro: @ano="2000", @marca="Gol", @modelo="Exemplo", @dono="Dono exemplo", @cor="Vermelho", @tipo="Tipo exemplo">

Mas e se quisermos também ter a opção de inicializar a classe Carro passando um bloco? Sem problemas, usamos o comando yield passando como parâmetro self se um bloco foi fornecido:

class Carro
  attr_accessor :ano, :marca, :modelo, :dono, :cor, :tipo

  def initialize(attributes = nil)
    attributes.each do |attr, value|
      self.send("#{attr}=", value)
    end unless attributes.nil?

    yield self if block_given?
  end
end

carro = Carro.new do |c|
  c.ano    = "2000"
  c.marca  = "Gol"
  c.modelo = "Exemplo"
  c.dono   = "Dono exemplo"
  c.cor    = "Vermelho"
  c.tipo   = "Tipo exemplo"
end

puts carro.inspect #<Carro: @ano="2000", @marca="Gol", @modelo="Exemplo", @dono="Dono exemplo", @cor="Vermelho", @tipo="Tipo exemplo">

Nessa implementação sempre irá prevalecer o que vier no bloco. Então se você passar um hash e um bloco para o construtor ao mesmo tempo (o que é uma bizarrice), os atributos que coincidirem terão o valor que foi passado no bloco:

carro = Carro.new(:ano => "2000", :marca => "Gol", :modelo => "Exemplo") do |c|
  c.modelo = "MODELO DO BLOCO"
  c.dono   = "Dono exemplo"
  c.cor    = "Vermelho"
  c.tipo   = "Tipo exemplo"
end

puts carro.inspect #<Carro: @ano="2000", @marca="Gol", @modelo="MODELO DO BLOCO", @dono="Dono exemplo", @cor="Vermelho", @tipo="Tipo exemplo">

Esses eram meus comentários sobre o interessante assunto de construtores em Ruby.

Ruby , , , , , , ,