Создание идиоматического объекта в рубине
В рубине я часто нахожу, что пишу следующее:
class Foo
def initialize(bar, baz)
@bar = bar
@baz = baz
end
<< more stuff >>
end
или даже
class Foo
attr_accessor :bar, :baz
def initialize(bar, baz)
@bar = bar
@baz = baz
end
<< more stuff >>
end
Я всегда стараюсь максимально свести к минимуму шаблон, так что есть ли более идиоматический способ создания объектов в рубине?
Ответы
Ответ 1
Один из вариантов заключается в том, что вы можете наследовать определение своего класса из 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"
Ответ 2
Я спросил повторяющийся вопрос и предложил свой собственный ответ там, ожидая лучшего, но удовлетворительного не появилось. Я выложу свой собственный.
Определите метод класса, подобный следующему, в духе методов 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
Затем вы можете использовать его следующим образом:
class Foo
attr_constructor :foo, :bar, :buz
end
p Foo.new('a', 'b', 'c') # => #<Foo:0x93f3e4c @foo="a", @bar="b", @buz="c">
p Foo.new('a', 'b', 'c', 'd') # => #<Foo:0x93f3e4d @foo="a", @bar="b", @buz="c">
p Foo.new('a', 'b') # => #<Foo:0x93f3e4e @foo="a", @bar="b", @buz=nil>
Ответ 3
Struct
Struct
объект - это классы, которые делают почти то, что вы хотите. Единственное различие заключается в том, что метод initialize имеет значение nil в качестве значения по умолчанию для всех аргументов. Вы используете его так:
A= Struct.new(:a, :b, :c)
или
class A < Struc.new(:a, :b, :c)
end
Struct
имеет один большой недостаток. Вы не можете наследовать из другого класса.
Напишите свой собственный спецификатор атрибутов
Вы можете написать свой собственный метод для указания атрибутов
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
Теперь ваш класс может наследовать другие классы. Единственный недостаток здесь - вы не можете получить количество аргументов для инициализации. Это то же самое для Struct
.
B.method(:initialize).arity # => -1
Ответ 4
Вы можете использовать Virtus, я не думаю, что это идиоматический способ сделать это, но он делает для вас все плиты котла.
require 'Virtus'
class Foo
include 'Virtus'
attribute :bar, Object
attribute :baz, Object
end
Затем вы можете делать такие вещи, как
foo = Foo.new(:bar => "bar")
foo.bar # => bar
Если вам не нравится передавать хэш инициализатору, добавьте:
def initialize(bar, baz)
super(:bar => bar, :baz => baz)
end
Если вы не думаете, что это достаточно DRY, вы также можете сделать
def initialize(*args)
super(self.class.attributes.map(&:name).zip(args)])
end
Ответ 5
Я иногда делаю
@bar, @baz = bar, baz
Еще шаблон, но он занимает только одну строку.
Я думаю, вы могли бы также сделать
["bar", "baz"].each do |variable_name|
instance_variable_set(:"@#{variable_name}", eval(variable_name))
end
(Я уверен, что есть менее опасный способ сделать это, заметьте)
https://bugs.ruby-lang.org/issues/5825 - это предложение сделать табличку менее подробной.
Ответ 6
Вы можете использовать объект как параметр.
class Foo
attr_accessor :param
def initialize(p)
@param = p
end
end
f = Foo.new
f.param.bar = 1
f.param.bax = 2
В этом случае это не сэкономит много строк, но будет, если ваш класс должен обрабатывать большое количество параметров. Вы также можете реализовать метод set_param и get_param, если хотите, чтобы ваш @param var был приватным.