如何在 Ruby 中使用 method_missing

How to use method_missing in Ruby

我有一个作业问题要为 Ruby 创建一个简单的 DSL 配置。

问题出在method_missing。我需要打印出键的值,但它们是自动打印出来的,而不是通过命令打印出来的。

init.rb:

require_relative "/home/marie/dsl/store_application.rb"

config = Configus.config do |app|
  app.environment = :production

  app.key1 = "value1"
  app.key2 = "value2"

  app.group1 do |group1|
    group1.key3 = "value3"
    group1.key4 = "value4"
  end
end

store_application.rb:

class Configus
  class << self
    def config
      yield(self)
    end

    #    attr_accessor :environment,
    #                  :key1,
    #                  :key2,
    #                  :key3,
    #                  :key4

    def method_missing(m, args)
      puts args
    end

    def group1(&block)
      @group1 ||= Group1.new(&block)
    end
  end

  class Group1
    class << self
      def new
        unless @instance
          yield(self)
        end
        @instance ||= self
      end

      # attr_accessor :key1,
      #               :key2,
      #               :key3,
      #               :key4

      def method_missing(m, *args)
        p m, args
      end
    end
  end
end

Ruby 的 init.rb 输出:

marie@marie:~/dsl$ ruby init.rb 
production
value1
value2
:key3=
["value3"]
:key4=
["value4"]

问题是值是自动打印的,我需要使用以下方法打印它们:

config.key1         => 'value1'
config.key2         => 'value2'
config::Group1.key3 => 'value3'
config::Group1.key4 => 'value4'

您的实施中有几处需要修正以符合您的期望:

1) config class 方法 returns 块执行的结果,因此在您的示例中 config 变量包含 Configus::Group1,而不是 Configus 如您所料。

2) method_missing 现在无论方法名称如何,其行为方式都完全相同。但很明显,您期望 setter 和 getter 有不同的行为。

所以一个天真的(和肮脏的)修复可能如下所示:

class Configus
  class << self    
    def config
      yield(self) if block_given?
      self
    end

    def method_missing(method_name, *args)
      @config_keys ||= {}

      if method_name.to_s.end_with?('=')
        @config_keys[method_name.to_s[0..-2].to_sym] = args
      elsif @config_keys.key?(method_name)
        @config_keys[method_name]
      else
        super
      end
    end

    # ...
  end

  # ...
end

(同样适用于 Group1,但我相信您也知道如何修复它)

不过,您的 DSL 还有一个实际问题:对嵌套设置的支持是硬编码的,这使得它不灵活。例如,您不能以这种方式构建嵌套层次结构,并且要引入新的嵌套组,您必须更改 class 定义(添加方法)。在 Ruby 中有很多方法可以解决这个问题,例如,我们可以使用 OpenStruct,它在幕后做了很多 method_missing 魔法,并因此稍微简化了代码。肮脏的例子:

require "singleton"

class Configus
  include Singleton

  class ParamSet < OpenStruct
    def method_missing(method_name, *args)
      # Naive, non-robust support for nested groups of settings
      if block_given?
        subgroup = self[method_name] || ParamSet.new
        yield(subgroup)
        self[method_name] = subgroup
      else
        super
      end
    end
  end

  def self.config
    yield(self.instance.config) if block_given?
    self.instance
  end

  def method_missing(method_name, *args)
    config.send(method_name, *args) || super
  end

  def config
    @config ||= ParamSet.new
  end
end

现在可以嵌套设置了,例如

config = Configus.config do |app|
  app.environment = :production

  app.key1 = "value1"
  app.key2 = "value2"

  app.group1 do |group1|
    group1.key3 = "value3"
    group1.key4 = "value4"
    group1.group2 do |group2|
      group2.key5 = "foo"
    end
  end
end

然后

config.key1 #=> "value1"
config.group1.key3 #=> "value3"
config.group1.group2.key5 #=> "foo"

P.S。还有一件事要提:经验法则是每次玩 method_missing 时定义适当的 respond_to_missing? (至少对于生产级代码)...