Ruby dry initialization with hash argument

I often use hash arguments for constructors, especially when writing DSL for configuration or other bits of the API that the end user will be exposed to. As a result, I am doing something like the following:

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 there a more idiomatic way to achieve this? The ejection constant and converting a character to a string seem particularly egregious.

+48
idioms initialization ruby dry
Apr 21 '10 at 5:23
source share
6 answers

You don't need a constant, but I don't think you can exclude a character in a string:

 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 

By the way, you can take a look (if you haven't already) in the Struct class generator class, it looks a bit like what you are doing, but not hash type initialization (but, I think, it would not be easy to create the corresponding generator class).

HasProperties

Trying to implement hurikhan idea, here is what I came to:

 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 

Since I am not good at metaprogramming, I responded to the community wiki so that anyone can change the implementation.

Struct.hash_initialized

Struct deployed the answer to Marc-Andre, here is the general Struct method for creating initialized hash classes:

 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 pm #=>#<struct MyClass name="asd", age=32> 
+73
Apr 21 '10 at 7:26
source share

Struct classes can help you build such a class. The initializer takes arguments one by one, and not as a hash, but it is easy to convert this:

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

If you want to stay more general, you can call values_at(*self.class.members) instead.

+30
Apr 22 '10 at 0:55
source share

There are some useful things in Ruby for this. The OpenStruct class will cause the values ​​of a to pass to its initialization. The method is available as class attributes.

 require 'ostruct' class InheritanceExample < OpenStruct end example1 = InheritanceExample.new(:some => 'thing', :foo => 'bar') puts example1.some # => thing puts example1.foo # => bar 

The documents are here: http://www.ruby-doc.org/stdlib-1.9.3/libdoc/ostruct/rdoc/OpenStruct.html

What if you do not want to inherit OpenStruct (or cannot, because you are already inheriting from something else)? You can delegate the whole method calls an instance of OpenStruct with 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 

Documents for sending: http://www.ruby-doc.org/stdlib-1.9.3/libdoc/forwardable/rdoc/Forwardable.html

+10
Jul 27 '12 at 9:30
source share

Given that your hashes will include ActiveSupport::CoreExtensions::Hash::Slice , there is a very nice 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 

I would cast it as a generic module that you could include, and which defines a has_properties method to set properties and perform proper initialization (this is untested, treat it like a 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 
+2
Apr 21 '10 at 8:11
source share

My solution is similar to Marc-André Lafortune. The difference is that each value is removed from the input hash since it is used to assign a member variable. Then, a class built with Struct can do further processing on everything that can be left in Hash. For example, JobRequest below saves any "additional" arguments from Hash in the options field.

 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
Jan 11 2018-12-12T00:
source share

Please take a look at my gem, Valuable :

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

I use it for everything from search classes (EmployeeSearch, TimeEntrySearch) to reports (EmployeesWhoDidNotClockOutReport, ExecutiveSummaryReport) for API endpoint presenters. If you add the ActiveModel bit, you can easily hook these classes up to forms to get the criteria. I hope you find this helpful.

+1
Jan 14 '15 at 20:37
source share



All Articles