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