DRY Ruby-initialisatie met hash-argument

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 Structklasse 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 Structgebaseerde 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 Structkan 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::Slicezouden 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.

Other episodes