Подтвердить что ты не робот

Сухая инициализация 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

Нет ли более идиоматического способа достичь этого? Константа выброса и преобразование символа в строку кажутся особенно вопиющими.

4b9b3361

Ответ 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, вы можете легко подключить эти классы до форм для получения критериев. Надеюсь, вы сочтете это полезным.