Inicjalizacja DRY Ruby z argumentem Hash

Często używam argumentów hash do konstruktorów, szczególnie podczas pisania DSL dla konfiguracji lub innych bitów API, na które użytkownik końcowy będzie narażony. To co robię to coś w stylu:

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

Czy nie ma już idiomatycznego sposobu, aby to osiągnąć? Stała odrzucenia i konwersja symbolu na ciąg wydają się szczególnie skandaliczne.

Author: Kyle Mitchell, 2010-04-21

6 answers

Nie potrzebujesz stałej, ale myślę, że nie możesz wyeliminować symbolu do ciągu:

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

BTW, możesz rzucić okiem (jeśli jeszcze nie masz) na Struct Klasa generatora klasy, jest nieco podobna do tego, co robisz, ale bez inicjalizacji typu hash (ale myślę, że nie byłoby trudno stworzyć odpowiednią klasę generatora).

HasProperties

Próbując zaimplementować pomysł hurikhana, oto do czego doszedłem:

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

Ponieważ nie jestem aż tak biegły w metaprogramowanie, zrobiłem wiki społeczności odpowiedzi więc każdy może zmienić implementację.

Struct.hash_initialized

Rozszerzając odpowiedź Marc-Andre, oto ogólna, oparta na Struct metoda tworzenia klas hashowych:

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>
 78
Author: Mladen Jablanović,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-04-22 08:38:25

Klasa Struct może pomóc ci zbudować taką klasę. Inicjalizator pobiera argumenty jeden po drugim zamiast jako hash, ale łatwo jest przekonwertować, że:

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

Jeśli chcesz pozostać bardziej ogólny, możesz zadzwonić values_at(*self.class.members) zamiast tego.

 31
Author: Marc-André Lafortune,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-24 22:58:03

Jest kilka przydatnych rzeczy w Ruby do robienia tego typu rzeczy. Klasa OpenStruct spowoduje, że wartości a zostaną przekazane do jej inicjalizacji metoda dostępna jako atrybuty w klasie.

require 'ostruct'

class InheritanceExample < OpenStruct
end

example1 = InheritanceExample.new(:some => 'thing', :foo => 'bar')

puts example1.some  # => thing
puts example1.foo   # => bar

Dokumenty są tutaj: http://www.ruby-doc.org/stdlib-1.9.3/libdoc/ostruct/rdoc/OpenStruct.html

Co jeśli nie chcesz dziedziczyć po OpenStruct (lub nie możesz, bo jesteś już dziedziczy po czymś innym)? Możesz delegować wszystkie metody połączenia do OpenStruct przykład z 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

Dokumenty dla Forwardable są tutaj: http://www.ruby-doc.org/stdlib-1.9.3/libdoc/forwardable/rdoc/Forwardable.html

 10
Author: Graham Ashton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-27 09:30:11

Biorąc pod uwagę, że Twój hash zawiera ActiveSupport::CoreExtensions::Hash::Slice, jest bardzo ładne rozwiązanie:

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

Chciałbym streścić to do generycznego modułu, który możesz dołączyć i który definiuje metodę "has_properties", aby ustawić właściwości i wykonać właściwą inicjalizację (nie jest to testowane, weź to jako pseudo kod):

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
 3
Author: hurikhan77,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-04-21 08:11:38

Moje rozwiązanie jest podobne do Marc-André Lafortune. Różnica polega na tym, że każda wartość jest usuwana z hasha wejściowego, ponieważ jest używana do przypisania zmiennej członkowskiej. Następnie Klasa pochodna Struct może wykonać dalsze przetwarzanie na tym, co może pozostać w Hash. Na przykład poniższe zapytanie JobRequest zachowuje wszelkie" dodatkowe " argumenty z Hasha w polu opcji.

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
 2
Author: kgilpin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-01-11 14:39:59

Proszę spojrzeć na mój klejnot, cenne :

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"}>

Używam go do wszystkiego, od klas wyszukiwania (EmployeeSearch, TimeEntrySearch) do raportowania (EmployeesWhoDidNotClockOutReport, ExecutiveSummaryReport) do prezenterów po punkty końcowe API. Jeśli dodasz kilka bitów ActiveModel, możesz łatwo podłączyć te klasy do formularzy w celu zebrania kryteriów. Mam nadzieję, że się przyda.

 1
Author: MustModify,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-05 08:47:51