这个问题概括了Ruby中动态扩展类层次结构的简单情况。
我遇到的问题是,我想用DSL定义这个子类,我认为我是自己复杂范围的受害者。
我有使用基类的工作代码:
module Command
class Command
...
end
end
然后每个命令都被实现为一个子类:
module Command
class Command_quit < Command
def initialize
name = "quit"
exec do
@user.client.should_terminate = true
end
end
end
end
这里有很多死记硬背和重复,我设想了一种DSL,它可以显著地消除这种情况:
module Command
define :quit do
exec do # this is global.rb:7 from the error below
@user.client.should_terminate = true
end
end
end
正如你所看到的,我想去掉样板,因为我只关心#initialize
的内容,它设置了一些元数据(如name
)并定义了exec
块(这是重要的部分)。
我被以下模块方法卡住了:
module Command
def self.define(cmd_name, &init_block)
class_name = "Command_#{cmd_name.to_s}"
class_definition = Class.new(Command)
class_initializer = Proc.new do
name = cmd_name
init_block.call
end
::Command.const_set class_name, class_definition
::Command.const_get(class_name).send(:define_method, :initialize, class_initializer)
end
end
此代码生成lib/commands/global.rb:7:in 'exec': wrong number of arguments (0 for 1+) (ArgumentError)
假设我有一些元数据(foo
),我想在DSL:中设置这些元数据
module Command
define :quit do
foo "bar" # this becomes global.rb:7
exec do
@user.client.should_terminate = true
end
end
end
我看到lib/commands/global.rb:7:in block in <module:Command>': undefined method 'foo' for Command:Module (NoMethodError)
我想我的Proc/block/lambda fu在这里错了,但我正在努力弄清混乱的根源。我应该如何编写Command::define
以获得所需的结果?看起来,尽管Ruby将Command::Command_help
创建为Command::Command
的子类,但它实际上并没有继承任何属性。
当您在Ruby中引用something
时,它首先在本地绑定中查找something
,如果失败,则查找self.something
。self
表示评估的上下文,该上下文在类定义class C; self; end
、方法定义class C; def m; self; end; end
上发生变化,但在块定义上不会发生变化。该块在块定义点捕获当前CCD_ 16。
module Command
define :quit do
foo "bar" # self is Command, calls Command.foo by default
end
end
如果要修改块内的self
上下文,可以使用BasicObject.instance_eval
(或instance_exec
、class_eval
、class_exec
)。
对于您的示例,传递给define
的块应该在具体命令实例的self
上下文下进行评估。
下面是一个例子。我在类Command::Command
:中添加了一些mock方法定义
module Command
class Command
# remove this accessor if you want to make `name` readonly
attr_accessor :name
def exec(&block)
@exec = block
end
def foo(msg)
puts "FOO => #{msg}"
end
def run
@exec.call if @exec
end
end
def self.define(name, &block)
klass = Class.new(Command) do
define_method(:initialize) do
method(:name=).call(name) # it would be better to make it readonly
instance_eval(&block)
end
# readonly
# define_method(:name) { name }
end
::Command.const_set("Command_#{name}", klass)
end
define :quit do
foo "bar"
exec do
puts "EXEC => #{name}"
end
end
end
quit = Command::Command_quit.new #=> FOO => bar
quit.run #=> EXEC => quit
puts quit.class #=> Command::Command_quit
您的问题是块保留self的值(除其他外)-当您调用init_block.call
并执行跳转到传递给define
的块时,self是模块Command
,而不是Command_quit
的实例
如果您将初始化方法更改为,您应该可以
class_initializer = Proc.new do
self.name = cmd_name # I assume you didn't just want to set a local variable
instance_eval(&init_block)
end
instance_eval
执行块,但使用接收器(在本例中,Command_quit
的实例作为子类。
"块保持自身"行为的一个例外是define_method
:在这种情况下,self将始终是调用该方法的对象,就像普通方法一样。