为 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.perform
和 initialize
的 arity,加上 #perform
class.
所以,我希望能够创建一个 ActiveSupport::Concern
(或者只是一个普通的 Module
,如果这样效果更好的话),它允许我做这样的事情:
class MyTestClass
inputs :foo, :bar
end
然后将使用一些元编程来定义上述方法的 self.perform
和 initialize
,其通风度将取决于 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))
在我的代码库中,我有一堆对象都遵循相同的接口,其中包含如下内容:
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.perform
和 initialize
的 arity,加上 #perform
class.
所以,我希望能够创建一个 ActiveSupport::Concern
(或者只是一个普通的 Module
,如果这样效果更好的话),它允许我做这样的事情:
class MyTestClass
inputs :foo, :bar
end
然后将使用一些元编程来定义上述方法的 self.perform
和 initialize
,其通风度将取决于 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))