Сухая инициализация Ruby с аргументом хеширования
Я довольно часто использую хэш-аргументы для конструкторов, особенно при написании DSL для конфигурации или других битов API, которым будет подвергаться конечный пользователь. В результате я делаю что-то вроде следующего:
class Example
PROPERTIES = [:name, :age]
PROPERTIES.each { |p| attr_reader p }
def initialize(args)
PROPERTIES.each do |p|
self.instance_variable_set "@#{p}", args[p] if not args[p].nil?
end
end
end
Нет ли более идиоматического способа достичь этого? Константа выброса и преобразование символа в строку кажутся особенно вопиющими.
Ответы
Ответ 1
Вам не нужна константа, но я не думаю, что вы можете исключить символ в строку:
class Example
attr_reader :name, :age
def initialize args
args.each do |k,v|
instance_variable_set("@#{k}", v) unless v.nil?
end
end
end
#=> nil
e1 = Example.new :name => 'foo', :age => 33
#=> #<Example:0x3f9a1c @name="foo", @age=33>
e2 = Example.new :name => 'bar'
#=> #<Example:0x3eb15c @name="bar">
e1.name
#=> "foo"
e1.age
#=> 33
e2.name
#=> "bar"
e2.age
#=> nil
Кстати, вы можете взглянуть (если у вас еще нет) в класс генератора классов Struct
, он несколько похож на что вы делаете, но не инициализации типа хэш-типа (но, я думаю, было бы нелегко создать соответствующий класс генератора).
HasProperties
Попытка реализовать идею hurikhan, вот что я пришел к:
module HasProperties
attr_accessor :props
def has_properties *args
@props = args
instance_eval { attr_reader *args }
end
def self.included base
base.extend self
end
def initialize(args)
args.each {|k,v|
instance_variable_set "@#{k}", v if self.class.props.member?(k)
} if args.is_a? Hash
end
end
class Example
include HasProperties
has_properties :foo, :bar
# you'll have to call super if you want custom constructor
def initialize args
super
puts 'init example'
end
end
e = Example.new :foo => 'asd', :bar => 23
p e.foo
#=> "asd"
p e.bar
#=> 23
Поскольку я не разбираюсь в метапрограммировании, я сделал ответную вики сообщества, чтобы любой мог изменить реализацию.
Struct.hash_initialized
Развернув ответ на Marc-Andre, вот общий метод Struct
для создания инициализированных хэш-классов:
class Struct
def self.hash_initialized *params
klass = Class.new(self.new(*params))
klass.class_eval do
define_method(:initialize) do |h|
super(*h.values_at(*params))
end
end
klass
end
end
# create class and give it a list of properties
MyClass = Struct.hash_initialized :name, :age
# initialize an instance with a hash
m = MyClass.new :name => 'asd', :age => 32
p m
#=>#<struct MyClass name="asd", age=32>
Ответ 2
Классы Struct
могут помочь вам построить такой класс. Инициализатор принимает аргументы один за другим, а не как хэш, но легко преобразовать это:
class Example < Struct.new(:name, :age)
def initialize(h)
super(*h.values_at(:name, :age))
end
end
Если вы хотите оставаться более общим, вы можете вызвать values_at(*self.class.members)
вместо этого.
Ответ 3
В Ruby есть некоторые полезные вещи для этого.
Класс OpenStruct приведет к тому, что значения a перешли к его инициализации
метод доступен как атрибуты класса.
require 'ostruct'
class InheritanceExample < OpenStruct
end
example1 = InheritanceExample.new(:some => 'thing', :foo => 'bar')
puts example1.some # => thing
puts example1.foo # => bar
Документы находятся здесь:
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/ostruct/rdoc/OpenStruct.html
Что делать, если вы не хотите наследовать OpenStruct (или не можете, потому что вы
уже наследуя от чего-то другого)? Вы можете делегировать весь метод
вызывает экземпляр OpenStruct с Forwardable.
require 'forwardable'
require 'ostruct'
class DelegationExample
extend Forwardable
def initialize(options = {})
@options = OpenStruct.new(options)
self.class.instance_eval do
def_delegators :@options, *options.keys
end
end
end
example2 = DelegationExample.new(:some => 'thing', :foo => 'bar')
puts example2.some # => thing
puts example2.foo # => bar
Документы для пересылки:
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/forwardable/rdoc/Forwardable.html
Ответ 4
Учитывая, что ваши хэши будут включать ActiveSupport::CoreExtensions::Hash::Slice
, есть очень приятное решение:
class Example
PROPERTIES = [:name, :age]
attr_reader *PROPERTIES #<-- use the star expansion operator here
def initialize(args)
args.slice(PROPERTIES).each {|k,v| #<-- slice comes from ActiveSupport
instance_variable_set "@#{k}", v
} if args.is_a? Hash
end
end
Я бы отринул это на общий модуль, который вы могли бы включить, и который определяет метод "has_properties" для установки свойств и выполнения правильной инициализации (это непроверено, воспринимайте его как псевдокод):
module HasProperties
def self.has_properties *args
class_eval { attr_reader *args }
end
def self.included base
base.extend InstanceMethods
end
module InstanceMethods
def initialize(args)
args.slice(PROPERTIES).each {|k,v|
instance_variable_set "@#{k}", v
} if args.is_a? Hash
end
end
end
Ответ 5
Мое решение похоже на Marc-André Lafortune. Разница в том, что каждое значение удаляется из входного хеша, поскольку используется для назначения переменной-члена. Затем класс, построенный с помощью Struct, может выполнять дальнейшую обработку на всех, что может быть оставлено в Hash. Например, JobRequest ниже сохраняет любые "дополнительные" аргументы из Hash в поле опций.
module Message
def init_from_params(params)
members.each {|m| self[m] ||= params.delete(m)}
end
end
class JobRequest < Struct.new(:url, :file, :id, :command, :created_at, :options)
include Message
# Initialize from a Hash of symbols to values.
def initialize(params)
init_from_params(params)
self.created_at ||= Time.now
self.options = params
end
end
Ответ 6
Пожалуйста, взгляните на мой драгоценный камень, Valuable:
class PhoneNumber < Valuable
has_value :description
has_value :number
end
class Person < Valuable
has_value :name
has_value :favorite_color, :default => 'red'
has_value :age, :klass => :integer
has_collection :phone_numbers, :klass => PhoneNumber
end
jackson = Person.new(name: 'Michael Jackson', age: '50', phone_numbers: [{description: 'home', number: '800-867-5309'}, {description: 'cell', number: '123-456-7890'})
> jackson.name
=> "Michael Jackson"
> jackson.age
=> 50
> jackson.favorite_color
=> "red"
>> jackson.phone_numbers.first
=> #<PhoneNumber:0x1d5a0 @attributes={:description=>"home", :number=>"800-867-5309"}>
Я использую его для всего, от классов поиска (EmployeeSearch, TimeEntrySearch) до отчетов (EmployeesWhoDidNotClockOutReport, ExecutiveSummaryReport) для презентаторов конечных точек API. Если вы добавите бит ActiveModel, вы можете легко подключить эти классы до форм для получения критериев. Надеюсь, вы сочтете это полезным.