5

フィールドが設定または読み取られたときに、フィールドのセットを指定されたRubyタイプに自動的に変換する単純なクラスを作成しようとしています。

これが私がこれまでに持っているものであり、それは機能します。ただし、これはDRYではなく、私のメタプログラミングは初歩的なものです。

これを実装するためのより良い、よりクリーンな方法はありますか?

class BasicModel

  def self.fields(params)
    params.each do |name, type|

      # Define field writers
      define_method("#{name}=") {|v| @fields[name] = v}

      # Define field readers
      case type.name
      when 'String'
        define_method(name) { @fields[name].to_s }
      when 'Integer'
        define_method(name) { @fields[name].to_i }
      when 'Float'
        define_method(name) { @fields[name].to_f }
      else raise 'invalid field type'
      end

    end
  end

  fields(
    name: String,
    qty: Integer,
    weight: Float
  )

  def initialize
    @fields = {}
  end

end

# specification
m = BasicModel.new
m.name         => ""
m.name = 2     => 2
m.name         => "2"
m.qty          => 0
m.qty = "1"    => "1"
m.qty          => 1
m.weight       => 0.0
m.weight = 10  => 10
m.weight       => 10.0

リーダーとライターの型キャストのデメリット/デメリットは何ですか?たとえば、次のコードは、リーダー(上記)ではなく、ライターでタイプキャストされます。中にも入れましcasedefine_method

class BasicModel
  def self.fields(params)
    params.each do |name, type|

      define_method(name) { @fields[name] }

      define_method("#{name}=") do |val|
        @fields[name] = case type.name
                        when 'Integer'  then val.to_i
                        when 'Float'    then val.to_f
                        when 'String'   then val.to_s
                        else raise 'invalid field type'
                        end
    end
  end
end

考えられる懸念は、決定木(例:caseステートメント)をブロックから除外する必要があることだと考えていましたdefine_method。フィールドが設定/読み取られるたびに、ステートメントが無意味に評価されると思います。これは正しいです?

4

3 に答える 3

4

そこで、ここで2つの質問をしました。

  1. メタプログラムで型キャストする方法
  2. リーダーとライターのどちらで型キャストするか。

2番目の質問は答えるのがはるかに簡単なので、そこから始めましょう。

私は作家にキャストします。なんで?違いは微妙ですが、リーダーにキャストすると、オブジェクト内での動作が多少異なります。

たとえば、priceタイプがのフィールドがありInteger、これを読み取り時にキャストした場合、クラス内でとの値は同じではpriceあり@fields['price']ません。リーダーメソッドを使用する必要があるため、これは大したことではありませんが、なぜ不必要な不整合を作成するのでしょうか。

最初の質問はもっと興味深いものです。メタプログラムで型キャストする方法です。あなたのコードは、rubyでの型強制の一般的なメソッド、つまりto_*ほとんどのオブジェクトが提供するメソッドを示しています。ただし、これを行う別の方法があります。

String(:hello!) #=> "Hello"
Integer("123") #=> 123
Float("123") #=> 123.0
Array("1,2,3") #=> ["1,2,3"]

さて、これらは興味深いものです。ここで行っているのは、のような名前のないメソッドをクラスで呼び出しているようです。これは、構文が引数String.()でどのように機能するかを示しています。[]ただし、そうではありません。という名前のメソッドを定義することはできません()。代わりに、これらは実際にはカーネルで定義されたメソッドです。

したがって、メタプログラムでそれらを呼び出すには2つの方法があります。最も単純なのは次のようなものです。

type = 'String'
Kernel.send(type,:hello) #=> "hello"

型キャストメソッドが存在しない場合は、を取得しNoMethodErrorます。

次のように、メソッドオブジェクトを取得して呼び出すこともできます。

type = 'String'
method(type).call(:hello) #=> "hello"

この場合、メソッドが存在しないと、NameErrorが発生します。

これらの唯一の本当の注意点は、すべてのメタプログラミングと同様に、何を公開するかを考えたいということです。ユーザー入力で属性を定義する機会がある場合type、悪意のあるユーザーが次のようなペイロードを送信する可能性があります。

{type: 'sleep', value: 9999}

そして今、あなたのコードはを呼び出すつもりですKernel.send('sleep',9999)、それはあなたにとって非常にひどいでしょう。したがって、これらの型の値が信頼できない当事者によって設定できるものではないことを確認するか、許可された型をホワイトリストに登録する必要があります。

その警告を念頭に置いて、以下はあなたの問題を解決するためのかなりエレガントな方法です:

class BasicModel
  def self.fields(hash={})
    hash.each do |name,type|
      define_method("#{name}"){ instance_variable_get "@#{name"} }
      define_method("#{name}=") {|val| instance_variable_set "@#{name}", Kernel.send(type,val) }
    end
  end

  fields name: String, qty: Integer, weight: Float
end

また、このようなメタプログラミングマクロが正しく機能するためにinitializeメソッドに依存している場合、個人的には気に入らないため、フィールドハッシュではなくインスタンス変数(、、)@nameを定義していることにも注意してください。@qty@weight

イニシャライザをオーバーライドする必要がない場合は、追加の利点があります。実際にこれをモジュールに抽出して、この動作を提供する任意のクラスに拡張できます。次の例を考えてみましょう。今回は、許可されたフィールドタイプにホワイトリストが追加されています。

module Fieldset
  TYPES = %w|String Integer Float|

  def self.fields(hash={})
    hash.each do |name,type|
      raise ArgumentError, "Invalid Field Type: #{type}" unless TYPES.include?(type)
      define_method("#{name}"){ instance_variable_get "@#{name"} }
      define_method("#{name}=") {|val| instance_variable_set "@#{name}", Kernel.send(type,val) }
    end
  end
end

class AnyModel
  extend Fieldset
  fields name: String, qty: Integer, weight: Float
end

素晴らしい質問です。この答えがあなたにいくつかの新しいアイデアを与えることを願っています!

于 2014-11-21T21:45:51.677 に答える
1

本当に必要なのは、各フィールドで使用される型キャストメソッドへの参照だけです。セッターメソッドを定義する前にタイプキャスティングメソッドを決定しsend、セッターが呼び出されたときにタイプキャスティングを実行するために使用できます。

次に例を示します。

class BasicModel
  def self.fields(params)
    params.each do |name, type|

      operator = case type.name
        when 'Integer'  then :to_i
        when 'Float'    then :to_f
        when 'String'   then :to_s
        else raise 'invalid field type'
      end

      define_method(name) { @fields[name] }

      define_method("#{name}=") do |val|
        @fields[name] = val.send(operator)
      end

    end
  end

  def initialize
    @fields = {}
  end
end
于 2012-10-25T15:51:25.787 に答える
0

@lastcanalからアイデアを得ました。これが、私が思いついたものです。

class BasicModel

  FieldTypes = Hash.new(StandardError.new('unsupported field type')).update(
    String   => :to_s,
    Integer  => :to_i,
    Float    => :to_f
  )

  def self.fields(params)
    params.each do |name, type|
      define_method("#{name}=") {|v| @fields[name] = v}
      define_method(name) { @fields[name].send(FieldTypes[type]) }
    end
  end

  def initialize
    @fields = {}
  end

end

BasicModel.fields(
  name: String,
  qty: Integer,
  weight: Float
)

属性リーダーでの型キャストは、未設定の属性に対しては返されません nil。代わりnilに、オブジェクトタイプ(つまりnil.to_i => 0)に変換されます。未設定の属性を取得nilするには、属性ライターでキャストを入力します。

于 2012-11-09T10:26:27.990 に答える