Creación de objetos idiomáticos en Ruby

En ruby, a menudo me encuentro escribiendo lo siguiente:

class Foo def initialize(bar, baz) @bar = bar @baz = baz end <> end 

o incluso

 class Foo attr_accessor :bar, :baz def initialize(bar, baz) @bar = bar @baz = baz end <> end 

Siempre estoy dispuesto a minimizar todo lo posible, así que ¿hay una forma más idiomática de crear objetos en Ruby?

Una opción es que puedes heredar tu definición de clase de Struct:

 class Foo < Struct.new(:bar, :baz) # << more stuff >> end f = Foo.new("bar value","baz value") f.bar #=> "bar value" f.baz #=> "baz value" 

Hice una pregunta duplicada y sugerí mi propia respuesta allí, esperando una mejor, pero no apareció una satisfactoria. Voy a publicar el mío.

Defina un método de clase como el siguiente a lo largo del espíritu de los attr_accessor , attr_reader , attr_writer .

 class Class def attr_constructor *vars define_method("initialize") do |*vals| vars.zip(vals){|var, val| instance_variable_set("@#{var}", val)} end end end 

Entonces, puedes usarlo así:

 class Foo attr_constructor :foo, :bar, :buz end p Foo.new('a', 'b', 'c') # => # p Foo.new('a', 'b', 'c', 'd') # => # p Foo.new('a', 'b') # => # 

Struct

Struct objetos de Struct son clases que hacen casi lo que quieres. La única diferencia es que el método de inicialización no tiene valor predeterminado para todos sus argumentos. Lo usas así

 A= Struct.new(:a, :b, :c) 

o

 class A < Struc.new(:a, :b, :c) end 

Struct tiene un gran inconveniente. No puedes heredar de otra clase.

Escribe tu propio especificador de atributos

Podrías escribir tu propio método para especificar atributos

 def attributes(*attr) self.class_eval do attr.each { |a| attr_accessor a } class_variable_set(:@@attributes, attr) def self.get_attributes class_variable_get(:@@attributes) end def initialize(*vars) attr= self.class.get_attributes raise ArgumentError unless vars.size == attr.size attr.each_with_index { |a, ind| send(:"#{a}=", vars[ind]) } super() end end end class A end class B < A attributes :a, :b, :c end 

Ahora tu clase puede heredar de otras clases. El único inconveniente aquí es que no se puede obtener el número de argumentos para inicializar. Esto es lo mismo para Struct .

 B.method(:initialize).arity # => -1 

Podrías usar Virtus , no creo que sea la manera idiomática de hacerlo, pero tiene todo el plato de la caldera para ti.

 require 'Virtus' class Foo include 'Virtus' attribute :bar, Object attribute :baz, Object end 

Entonces puedes hacer cosas como

 foo = Foo.new(:bar => "bar") foo.bar # => bar 

Si no desea pasar un hash al inicializador, agregue:

 def initialize(bar, baz) super(:bar => bar, :baz => baz) end 

Si no crees que es lo suficientemente SECO, también puedes hacer

 def initialize(*args) super(self.class.attributes.map(&:name).zip(args)]) end 

A veces lo hago

 @bar, @baz = bar, baz 

Todavía es un repetitivo, pero solo ocupa una línea.

Supongo que también podrías hacer

 ["bar", "baz"].each do |variable_name| instance_variable_set(:"@#{variable_name}", eval(variable_name)) end 

(Estoy seguro de que hay una forma menos peligrosa de hacer eso, fíjate)

https://bugs.ruby-lang.org/issues/5825 es una propuesta para hacer que el texto repetitivo sea menos detallado.

Podrías usar un objeto como param.

 class Foo attr_accessor :param def initialize(p) @param = p end end f = Foo.new f.param.bar = 1 f.param.bax = 2 

Esto no ahorra muchas líneas en este caso, pero lo hará si su clase tiene que manejar una gran cantidad de param. También puede implementar un método set_param y get_param si desea mantener su @param var en privado.