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 ActiveRecord, Construtores, Design Patterns, DSL, Factory Method, Rails, Ruby, Ruby on Rails
Comentários