web-dev-qa-db-fra.com

DRY Ruby Initialisation avec l'argument de hachage)

Je me retrouve à utiliser des arguments de hachage pour les constructeurs un peu, en particulier lors de l'écriture de DSL pour la configuration ou d'autres bits d'API auxquels l'utilisateur final sera exposé. Je finis par faire quelque chose comme ceci:

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

N'y a-t-il pas de moyen plus idiomatique pour y parvenir? La constante jetable et la conversion de symbole en chaîne semblent particulièrement flagrantes.

53
Kyle E. Mitchell

Vous n'avez pas besoin de la constante, mais je ne pense pas que vous puissiez éliminer le symbole en chaîne:

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, vous pourriez jeter un œil (si vous ne l'avez pas déjà fait) à la classe du générateur de classe Struct , elle est quelque peu similaire à ce que vous faites, mais pas d'initialisation de type hachage ( mais je suppose qu'il ne serait pas difficile de créer une classe de générateur adéquate).

HasProperties

En essayant de mettre en œuvre l'idée d'Hurikhan, voici à quoi je suis parvenu:

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

Comme je ne suis pas très compétent en métaprogrammation, j'ai créé le wiki de la communauté de réponse pour que tout le monde soit libre de changer l'implémentation.

Struct.hash_initialized

En développant la réponse de Marc-Andre, voici une méthode générique, basée sur Struct pour créer des classes initialisées par hachage:

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>
81
Mladen Jablanović

Le Struct clas peut vous aider à construire une telle classe. L'initialiseur prend les arguments un par un au lieu d'un hachage, mais il est facile de le convertir:

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

Si vous souhaitez rester plus générique, vous pouvez appeler values_at(*self.class.members) à la place.

32

Il y a quelques choses utiles dans Ruby pour faire ce genre de chose. La classe OpenStruct rendra les valeurs de a passées à sa méthode initialize disponibles comme attributs sur la classe.

require 'ostruct'

class InheritanceExample < OpenStruct
end

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

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

Les documents sont ici: http://www.Ruby-doc.org/stdlib-1.9.3/libdoc/ostruct/rdoc/OpenStruct.html

Que faire si vous ne voulez pas hériter d'OpenStruct (ou ne le pouvez pas, car vous héritez déjà de quelque chose d'autre)? Vous pouvez déléguer tous les appels de méthode à une instance OpenStruct avec 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

Les documents pour Forwardable sont ici: http://www.Ruby-doc.org/stdlib-1.9.3/libdoc/forwardable/rdoc/Forwardable.html

10
Graham Ashton

Étant donné que vos hachages comprendraient ActiveSupport::CoreExtensions::Hash::Slice, il existe une très belle solution:

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

Je résumerais cela à un module générique que vous pourriez inclure et qui définit une méthode "has_properties" pour définir les propriétés et faire l'initialisation appropriée (ceci n'est pas testé, prenez-le comme pseudo code):

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
hurikhan77

Ma solution est similaire à Marc-André Lafortune. La différence est que chaque valeur est supprimée du hachage d'entrée car elle est utilisée pour affecter une variable membre. Ensuite, la classe dérivée de Struct peut effectuer un traitement supplémentaire sur tout ce qui peut rester dans le hachage. Par exemple, la JobRequest ci-dessous conserve tous les arguments "supplémentaires" du Hash dans un champ d'options.

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
kgilpin

S'il vous plaît jetez un oeil à mon joyau, précieux :

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

Je l'utilise pour tout, des classes de recherche (EmployeeSearch, TimeEntrySearch) aux rapports (EmployeesWhoDidNotClockOutReport, ExecutiveSummaryReport), des présentateurs aux points de terminaison API. Si vous ajoutez des bits ActiveModel, vous pouvez facilement connecter ces classes à des formulaires pour collecter des critères. J'espère que tu trouves cela utile.

1
MustModify