如何在 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?
(至少对于生产级代码)...
我有一个作业问题要为 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?
(至少对于生产级代码)...