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

对读者和作者进行类型转换的缺点/优点是什么?例如,以下代码在 writer 上进行类型转换,而不是在 reader 上(上图)。我也把case里面的define_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

我在想一个可能的问题是决策树(例如案例语句)可能应该被排除在define_method. 我假设每次设置/读取字段时都会对语句进行毫无意义的评估。这个对吗?

4

3 回答 3

4

所以,你在这里问了两个问题:

  1. 如何以元编程的方式进行类型转换
  2. 是否在阅读器或作者上进行类型转换。

第二个问题更容易回答,所以让我从这里开始:

我会投给作家。为什么?虽然差异是微妙的,但如果您对阅读器进行投射,您在对象内部的行为会有所不同。

例如,如果您有一个price类型为 的字段Integer,并且您在读取时将其强制转换,则在类内部 和 的值price@fields['price']一样。这没什么大不了的,因为您应该只使用 reader 方法,但是为什么要创建不必要的不​​一致呢?

第一个问题更有趣,如何以元编程的方式进行类型转换。您的代码说明了 ruby​​ 中类型强制的常用方法,即to_*大多数对象提供的方法。不过,还有另一种方法:

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

现在,这些很有趣。看起来您在这里所做的是在类上调用无名方法,例如String.(),这就是[]语法在参数上的工作方式。但事实并非如此,你不能定义一个名为(). 相反,这些实际上是在 Kernel 上定义的方法

因此,有两种元编程方式调用它们。最简单的是这样的:

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

另请注意,我定义的是实例变量 ( @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

您真正需要的只是对用于每个字段的类型转换方法的引用。您可以在定义 setter 方法之前确定类型转换方法,并send在调用 setter 时使用它来进行类型转换。

这是一个例子:

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 回答