如何在Ruby中动态声明子类?

时间:2014-04-21 03:02:45

标签: ruby inheritance scope dsl proc

This question几乎总结了在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)并定义了{{1}阻止(这是重要的部分)。

我遇到了以下模块方法:

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)),我想在我的DSL中设置:

foo

我看到module Command define :quit do foo "bar" # this becomes global.rb:7 exec do @user.client.should_terminate = true end end end

我想我的Proc / block / lambda-fu错了,但是我很难找到困惑的底线。我该如何写lib/commands/global.rb:7:in block in <module:Command>': undefined method 'foo' for Command:Module (NoMethodError)来获得理想的结果?看起来虽然Ruby创建Command::define作为Command::Command_help的子类,但它实际上并没有继承任何属性。

2 个答案:

答案 0 :(得分:1)

你的问题是块保留了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实例作为子类。

&#34;块的例外保留了自我&#34;行为是define_method:在这种情况下,self始终是调用方法的对象,就像使用普通方法一样。

答案 1 :(得分:1)

当您在Ruby中引用something时,它首先在本地绑定中查找something,如果失败,则会查找self.somethingself表示评估的上下文,此上下文在类定义class C; self; end,方法定义class C; def m; self; end; end上更改,但是,它不会在块定义上更改。该块在块定义点捕获当前self

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中添加了一些模拟方法定义:

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