DRY Ruby Initialization with Hash Argument

后端 未结 6 2005

I find myself using hash arguments to constructors quite a bit, especially when writing DSLs for configuration or other bits of API that the end user will be exposed to. Wha

相关标签:
6条回答
  • 2020-11-29 17:26

    There are some useful things in Ruby for doing this kind of thing. The OpenStruct class will make the values of a has passed to its initialize method available as attributes on the class.

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

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

    What if you don't want to inherit from OpenStruct (or can't, because you're already inheriting from something else)? You could delegate all method calls to an OpenStruct instance 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
    

    Docs for Forwardable are here: http://www.ruby-doc.org/stdlib-1.9.3/libdoc/forwardable/rdoc/Forwardable.html

    0 讨论(0)
  • 2020-11-29 17:29

    The Struct clas can help you build such a class. The initializer takes the arguments one by one instead of as a hash, but it's easy to convert that:

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

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

    0 讨论(0)
  • 2020-11-29 17:32

    Given your hashes would 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 abstract this to a generic module which you could include and which defines a "has_properties" method to set the properties and do the proper initialization (this is untested, take it as 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 讨论(0)
  • 2020-11-29 17:47

    You don't need the constant, but I don't think you can eliminate symbol-to-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
    

    BTW, you might take a look (if you haven't already) at the Struct class generator class, it's somewhat similar to what you are doing, but no hash-type initialization (but I guess it wouldn't be hard to make adequate generator class).

    HasProperties

    Trying to implement hurikhan's idea, this 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
    

    As I'm not that proficient with metaprogramming, I made the answer community wiki so anyone's free to change the implementation.

    Struct.hash_initialized

    Expanding on Marc-Andre's answer, here is a generic, Struct based method to create hash-initialized 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
    p m
    #=>#<struct MyClass name="asd", age=32>
    
    0 讨论(0)
  • 2020-11-29 17:49

    My solution is similar to Marc-André Lafortune. The difference is that each value is deleted from the input hash as it is used to assign a member variable. Then the Struct-derived class can perform further processing on whatever may be left in the Hash. For instance, the JobRequest below retains any "extra" arguments from the Hash in an 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
    
    0 讨论(0)
  • 2020-11-29 17:52

    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 reporting ( EmployeesWhoDidNotClockOutReport, ExecutiveSummaryReport) to presenters to API endpoints. If you add some ActiveModel bits you can easily hook these classes up to forms for gathering criteria. I hope you find it useful.

    0 讨论(0)
提交回复
热议问题