DRY Ruby Initialization с помощью хеш-аргумента - PullRequest
52 голосов
/ 21 апреля 2010

Я довольно часто использую аргументы хеш-функции для конструкторов, особенно когда пишу 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

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

Ответы [ 6 ]

79 голосов
/ 21 апреля 2010

Вам не нужна константа, но я не думаю, что вы можете исключить символ в строку:

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

Пытаясь реализовать идею Хурихана, вот к чему я пришел:

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

В продолжение ответа Марка-Андре приведем общий 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>
31 голосов
/ 22 апреля 2010

Класс Struct может помочь вам создать такой класс. Инициализатор принимает аргументы один за другим вместо хеша, но это легко конвертировать:

class Example < Struct.new(:name, :age)
    def initialize(h)
        super(*h.values_at(:name, :age))
    end
end

Если вы хотите остаться более универсальным, вы можете вместо этого позвонить values_at(*self.class.members).

10 голосов
/ 27 июля 2012

В 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 / пересылаемый / RDoc / Forwardable.html

3 голосов
/ 21 апреля 2010

Учитывая, что ваши хеши будут включать 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
2 голосов
/ 11 января 2012

Мое решение похоже на Marc-André Lafortune. Разница в том, что каждое значение удаляется из входного хэша, поскольку оно используется для назначения переменной-члена. Затем производный от Struct класс может выполнять дальнейшую обработку всего, что может быть оставлено в хеше. Например, приведенный ниже JobRequest сохраняет любые «дополнительные» аргументы из хэша в поле параметров.

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
1 голос
/ 14 января 2015

Пожалуйста, посмотрите на мой драгоценный камень, Ценный :

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...