54 votes

Initialisation DRY Ruby avec un argument Hash

J'utilise souvent des arguments de hachage pour les constructeurs, en particulier lorsque j'écris des DSL pour la configuration ou d'autres éléments de l'API auxquels l'utilisateur final sera exposé. Ce que je fais finalement, c'est quelque chose comme ce qui suit :

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 une façon plus idiomatique d'y parvenir ? La constante à jeter et la conversion de symbole en chaîne de caractères semblent particulièrement flagrantes.

81voto

Mladen Jablanović Points 22082

Vous n'avez pas besoin de la constante, mais je ne pense pas que vous puissiez éliminer le passage d'un symbole à une 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 coup d'œil (si vous ne l'avez pas déjà fait) à la rubrique Struct classe générateur, c'est un peu similaire à ce que vous faites, mais pas d'initialisation de type hash (mais je suppose qu'il ne serait pas difficile de faire une classe générateur adéquate).

HasProperties

En essayant de mettre en œuvre l'idée de hurikhan, voici ce à quoi je suis arrivé :

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 matière de métaprogrammation, j'ai fait de la réponse un wiki communautaire, de sorte que tout le monde est libre de modifier l'implémentation.

Struct.hash_initialized

En complément de la réponse de Marc-André, voici un générique, Struct pour créer des classes initialisées par le hash :

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>

0 votes

De cette façon, vous pouvez définir des variables d'instance arbitraires - c'est probablement la raison pour laquelle l'OP a utilisé une constante.

1 votes

@hurikhan77 : C'est vrai. BTW, j'ai essayé d'implémenter l'idée de HasProperties que vous avez suggérée, j'espère que cela ne vous dérange pas.

0 votes

Merci d'avoir pris le temps. En ce qui concerne le problème des ivars arbitraires : tel qu'il se présente, il fait partie de mes contraintes dans la situation actuelle. Il semble également que nous ayons contourné la constante en utilisant un ivar. Ai-je raison de supposer qu'il y aura une pénalité de mémoire ?

35voto

En Struct clas peut vous aider à mettre en place une telle classe. L'initialisateur 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) au lieu de cela.

0 votes

Cool ! Je l'ai un peu généralisé et je l'ai ajouté à ma réponse en tant que méthode Struct#hash_initialized, j'espère que cela ne vous dérange pas.

0 votes

Cool Marc-André

11voto

Graham Ashton Points 444

Il y a des choses utiles en Ruby pour faire ce genre de choses. La classe OpenStruct rendra les valeurs d'un has passé à sa méthode initialize en tant qu'attributs de 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 si vous ne pouvez pas, parce que vous êtes de quelque chose d'autre) ? Vous pouvez déléguer tous les appels de méthode à une instance d'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 relatifs à Forwardable sont disponibles ici : http://www.ruby-doc.org/stdlib-1.9.3/libdoc/forwardable/rdoc/Forwardable.html

3voto

hurikhan77 Points 3868

Étant donné que vos hachages comprendraient ActiveSupport::CoreExtensions::Hash::Slice Il existe une solution très intéressante :

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 ferais abstraction de cela dans un module générique que vous pourriez inclure et qui définirait une méthode "has_properties" pour définir les propriétés et effectuer l'initialisation appropriée (ceci n'est pas testé, considérez-le comme un 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

0 votes

Merci pour l'indication *ARRAY. J'avais réussi à ne pas voir ce morceau de sucre jusqu'à présent.

2voto

kgilpin Points 828

Ma solution est similaire à celle de Marc-André Lafortune. La différence est que chaque valeur est supprimée du hachage d'entrée lorsqu'elle est utilisée pour assigner une variable membre. Ensuite, la classe dérivée de la structure peut effectuer d'autres traitements sur ce qui reste dans le hachage. Par exemple, le JobRequest ci-dessous conserve tous les arguments "supplémentaires" du hachage 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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X