如何在Ruby中动态声明子类



这个问题概括了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.somethingself表示评估的上下文,该上下文在类定义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_execclass_evalclass_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将始终是调用该方法的对象,就像普通方法一样。

相关内容

  • 没有找到相关文章

最新更新