为 Ruby class 动态定义对象#initialize

Dynamically defining a Object#initialize for a Ruby class

在我的代码库中,我有一堆对象都遵循相同的接口,其中包含如下内容:

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

classes 之间的区别因素是 self.performinitializearity,加上 #perform class.

所以,我希望能够创建一个 ActiveSupport::Concern(或者只是一个普通的 Module,如果这样效果更好的话),它允许我做这样的事情:

class MyTestClass
  inputs :foo, :bar
end

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

这是我目前的情况:

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 的主体进行元编程,使其表现得像我提供的示例一样吗?

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

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

但是,我不会对它进行字符串评估。它通常会导致邪恶的事情。也就是说,这是一个给出不正确的 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]

你们太亲密了! instance_variable_set 接受两个参数,第一个是实例变量,第二个是您要将其设置为的值。您还需要获取变量的值,您可以使用 send.

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