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.
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>
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.
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
É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
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
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.