特にエンドユーザーがさらされる設定やその他の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
これを達成するためのこれ以上の慣用的な方法はありませんか?スローアウェイ定数と記号から文字列への変換は、特にひどいようです。
定数は必要ありませんが、シンボルから文字列を削除できるとは思いません。
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>
Struct
クラスは、このようなクラスを構築するのに役立ちます。初期化子は、ハッシュではなく引数を1つずつ受け取りますが、変換は簡単です。
_class Example < Struct.new(:name, :age)
def initialize(h)
super(*h.values_at(:name, :age))
end
end
_
より汎用的にしたい場合は、代わりにvalues_at(*self.class.members)
を呼び出すことができます。
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
ハッシュには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
私のソリューションは、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
私の宝石 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ビットを追加すると、これらのクラスを条件に収集するためのフォームに簡単に接続できます。役に立つと思います。