Ik merk dat ik nogal wat hash-argumenten voor constructeurs gebruik, vooral bij het schrijven van DSL’s voor configuratie of andere stukjes API waaraan de eindgebruiker wordt blootgesteld. Wat ik uiteindelijk doe is zoiets als het volgende:
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
Is er geen meer idiomatische manier om dit te bereiken? De wegwerpconstante en de conversie van symbool naar string lijken bijzonder flagrant.
Antwoord 1, autoriteit 100%
Je hebt de constante niet nodig, maar ik denk niet dat je symbool-naar-tekenreeks kunt elimineren:
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
Trouwens, je zou eens kunnen kijken (als je dat nog niet hebt gedaan) op de Struct
klasse generatorklasse, het is enigszins vergelijkbaar met wat u doet, maar geen hash-type initialisatie (maar ik denk dat het niet moeilijk zou zijn om een adequate generatorklasse te maken).
HasProperties
Ik probeerde het idee van hurikhan te implementeren, dit is waar ik op uitkwam:
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
Omdat ik niet zo bedreven ben in metaprogrammeren, heb ik de antwoordcommunity-wiki gemaakt, zodat iedereen de implementatie kan wijzigen.
Struct.hash_initialized
Voortbouwend op Marc-Andre’s antwoord, hier is een generieke, op Struct
gebaseerde methode om hash-geïnitialiseerde klassen te maken:
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>
Antwoord 2, autoriteit 44%
De clas Struct
kan je helpen zo’n klasse te bouwen. De initializer neemt de argumenten één voor één op in plaats van als een hash, maar het is eenvoudig om dat om te zetten:
class Example < Struct.new(:name, :age)
def initialize(h)
super(*h.values_at(:name, :age))
end
end
Als u algemener wilt blijven, kunt u in plaats daarvan values_at(*self.class.members)
aanroepen.
Antwoord 3, autoriteit 13%
Er zijn een aantal handige dingen in Ruby om dit soort dingen te doen.
De klasse OpenStruct zorgt ervoor dat de waarden van a zijn doorgegeven aan zijn initialisatie
methode beschikbaar als attributen voor de klasse.
require 'ostruct'
class InheritanceExample < OpenStruct
end
example1 = InheritanceExample.new(:some => 'thing', :foo => 'bar')
puts example1.some # => thing
puts example1.foo # => bar
De documenten zijn hier:
http://www.ruby-doc.org/stdlib-1.9. 3/libdoc/ostruct/rdoc/OpenStruct.html
Wat als u niet wilt overnemen van OpenStruct (of niet kunt, omdat u
al van iets anders erven)? Je zou alle methoden kunnen delegeren
oproepen naar een OpenStruct-instantie met 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
Documenten voor Forwardable vindt u hier:
http://www.ruby-doc.org/stdlib-1.9. 3/libdoc/forwardable/rdoc/Forwardable.html
Antwoord 4, autoriteit 4%
Aangezien uw hashes ActiveSupport::CoreExtensions::Hash::Slice
zouden bevatten, is er een zeer mooie oplossing:
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
Ik zou dit samenvatten tot een generieke module die je zou kunnen opnemen en die een “has_properties”-methode definieert om de eigenschappen in te stellen en de juiste initialisatie uit te voeren (dit is niet getest, neem het als 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
Antwoord 5, autoriteit 2%
Mijn oplossing is vergelijkbaar met Marc-André Lafortune. Het verschil is dat elke waarde wordt verwijderd uit de invoerhash omdat deze wordt gebruikt om een lidvariabele toe te wijzen. Dan kan de van Struct afgeleide klasse verdere verwerking uitvoeren op wat er nog in de Hash achterblijft. Het JobRequest hieronder behoudt bijvoorbeeld eventuele “extra” argumenten uit de hash in een optieveld.
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
Antwoord 6
Kijk eens naar mijn juweeltje, Waardevol:
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"}>
Ik gebruik het voor alles, van zoekklassen (EmployeeSearch, TimeEntrySearch) tot rapportage ( EmployeesWhoDidNotClockOutReport, ExecutiveSummaryReport) tot presentatoren tot API-eindpunten. Als je wat ActiveModel-bits toevoegt, kun je deze klassen gemakkelijk koppelen aan formulieren voor het verzamelen van criteria. Ik hoop dat je het nuttig vindt.