为Ruby类动态定义Object#initialize

时间:2018-03-06 15:03:20

标签: ruby-on-rails ruby metaprogramming

在我的代码库中,我有一堆对象都遵循相同的接口,包括这样的东西:

class MyTestClass
  def self.perform(foo, bar)
    new(foo, bar).perform
  end

  def initialize(foo, bar)
    @foo = foo
    @bar = bar
  end

  def perform
    # DO SOMETHING AND CHANGE THE WORLD
  end
end

类之间的区别因素是self.performinitialize arity ,以及#perform类的主体。

所以,我希望能够创建一个ActiveSupport::Concern(或者只是一个普通的Module,如果这会更好),这样我就可以做到这样的事情:

class MyTestClass
  inputs :foo, :bar
end

然后会使用一些元编程来定义上述方法的self.performinitialize,其中airty将取决于self.inputs方法指定的airty。

这是我到目前为止所做的:

module Commandable
  extend ActiveSupport::Concern

  class_methods do
    def inputs(*args)
      @inputs = args
      class_eval %(
        class << self
          def perform(#{args.join(',')})
            new(#{args.join(',')}).perform
          end
        end

        def initialize(#{args.join(',')})
          args.each do |arg|
            instance_variable_set(@#{arg.to_s}) = arg.to_s
          end
        end
      )

      @inputs
    end
  end
end

这似乎使方法的 arity 正确,但我很难弄清楚如何处理#initialize方法的主体。

任何人都可以帮我找出一种方法,我可以成功地对#initialize的主体进行元编程,这样它的行为就像我提供的例子一样吗?

2 个答案:

答案 0 :(得分:1)

您可以将其用作#initialize的正文:

#{args}.each { |arg| instance_variable_set("@\#{arg}", arg) }

但是,我不会将它评估为eval。它通常导致邪恶的事情。也就是说,这是一个实现,它给出了一个不正确的Foo.method(:perform).arity,但仍然表现得像你期望的那样:

module Commandable
  def inputs(*arguments)
    define_method(:initialize) do |*parameters|
      unless arguments.size == parameters.size
        raise ArgumentError, "wrong number of arguments (given #{parameters.size}, expected #{arguments.size})"
      end

      arguments.zip(parameters).each do |argument, parameter|
        instance_variable_set("@#{argument}", parameter)
      end
    end

    define_singleton_method(:perform) do |*parameters|
      unless arguments.size == parameters.size
        raise ArgumentError, "wrong number of arguments (given #{parameters.size}, expected #{arguments.size})"
      end

      new(*parameters).perform
    end
  end
end

class Foo
  extend Commandable
  inputs :foo, :bar

  def perform
    [@foo, @bar]
  end
end

Foo.perform 1, 2 # => [1, 2]

答案 1 :(得分:1)

你太近了! instance_variable_set有两个参数,第一个是实例变量,第二个是你想要设置它的值。您还需要获取变量的值,您可以使用send

来完成
instance_variable_set(@#{arg.to_s}, send(arg.to_s))