web-dev-qa-db-ja.com

DRY Rubyハッシュ引数による初期化

特にエンドユーザーがさらされる設定やその他のAPIのビットをDSLで書くときは、コンストラクターにハッシュ引数をかなり使用しています。私がやることは次のようなものです:

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

これを達成するためのこれ以上の慣用的な方法はありませんか?スローアウェイ定数と記号から文字列への変換は、特にひどいようです。

53

定数は必要ありませんが、シンボルから文字列を削除できるとは思いません。

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

ところで、 Struct クラスジェネレータークラスを(まだお持ちでない方は)ご覧になるかもしれませんが、これはやっていることと似ていますが、ハッシュ型の初期化はありません(しかし、適切なジェネレータークラスを作成するのは難しくないと思います)。

HasProperties

Hurikhanのアイデアを実装しようとして、これが私がやったことです。

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

私はメタプログラミングにそれほど詳しくないので、誰でも自由に実装を変更できるように、アンサーコミュニティwikiを作成しました。

Struct.hash_initialized

Marc-Andreの答えを拡張して、ハッシュ初期化クラスを作成する一般的なStructベースのメソッドを次に示します。

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

Structクラスは、このようなクラスを構築するのに役立ちます。初期化子は、ハッシュではなく引数を1つずつ受け取りますが、変換は簡単です。

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

より汎用的にしたい場合は、代わりにvalues_at(*self.class.members)を呼び出すことができます。

32

Rubyにはこの種のことを行うのに役立つものがあります。OpenStructクラスは、その初期化メソッドに渡されたaの値をクラスの属性として使用可能にします。

require 'ostruct'

class InheritanceExample < OpenStruct
end

example1 = InheritanceExample.new(:some => 'thing', :foo => 'bar')

puts example1.some  # => thing
puts example1.foo   # => bar

ドキュメントはこちらです: http://www.Ruby-doc.org/stdlib-1.9.3/libdoc/ostruct/rdoc/OpenStruct.html

OpenStructから継承したくない場合(または、既に何か他のものから継承しているため、継承できない場合)はどうでしょうか。 Forwardableを使用して、すべてのメソッド呼び出しをOpenStructインスタンスに委任できます。

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

Forwardableのドキュメントはこちら: http://www.Ruby-doc.org/stdlib-1.9.3/libdoc/forwardable/rdoc/Forwardable.html

10
Graham Ashton

ハッシュにはActiveSupport::CoreExtensions::Hash::Slice、非常に素晴らしい解決策があります。

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

これを抽象化して、含めることができ、プロパティを設定して適切な初期化を行う「has_properties」メソッドを定義する汎用モジュールに抽象化します(これはテストされていません。擬似コードとして使用してください)。

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
3
hurikhan77

私のソリューションは、Marc-AndréLafortuneに似ています。違いは、メンバー変数の割り当てに使用されるため、各値が入力ハッシュから削除されることです。その後、Struct派生クラスは、ハッシュに残っている可能性のあるものについてさらに処理を実行できます。たとえば、以下のJobRequestは、オプションフィールドのハッシュからの「余分な」引数を保持します。

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
kgilpin

私の宝石 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"}>

検索クラス(EmployeeSearch、TimeEntrySearch)からレポート(EmployeesWhoDidNotClockOutReport、ExecutiveSummaryReport)、プレゼンター、APIエンドポイントまで、すべてに使用します。 ActiveModelビットを追加すると、これらのクラスを条件に収集するためのフォームに簡単に接続できます。役に立つと思います。

1
MustModify