这是一些简单的代码,对于指定的每个参数,将添加以该参数命名的特定get / set方法。如果您撰写attr_option :foo, :bar
,那么您会在#foo/foo=
上看到#bar/bar=
和Config
个实例方法:
module Configurator
class Config
def initialize()
@options = {}
end
def self.attr_option(*args)
args.each do |a|
if not self.method_defined?(a)
define_method "#{a}" do
@options[:"#{a}"] ||= {}
end
define_method "#{a}=" do |v|
@options[:"#{a}"] = v
end
else
throw Exception.new("already have attr_option for #{a}")
end
end
end
end
end
到目前为止,这么好。我想写一些RSpec测试来验证这段代码实际上正在做它应该做的事情。但是有一个问题!如果我在其中一个测试方法中调用attr_option :foo
,那么现在可以在Config中永久定义该方法。因此,后续测试将失败,因为foo
已经定义:
it "should support a specified option" do
c = Configurator::Config
c.attr_option :foo
# ...
end
it "should support multiple options" do
c = Configurator::Config
c.attr_option :foo, :bar, :baz # Error! :foo already defined
# by a previous test.
# ...
end
有没有办法可以为每个测试提供一个Config
类的匿名“克隆”,它独立于其他类?
答案 0 :(得分:5)
“克隆”Config
类的一种非常简单的方法是使用匿名类对其进行子类化:
c = Class.new Configurator::Config
c.attr_option :foo
d = Class.new Configurator::Config
d.attr_option :foo, :bar
这对我来说没有错误。这是有效的,因为所有设置的实例变量和方法都与匿名类相关联,而不是Configurator::Config
。
语法Class.new Foo
创建一个匿名类,Foo
作为超类。
另外,Ruby中的throw
Exception
是不正确的; Exception
是raise
d。 throw
旨在像goto
一样使用,例如打破多个巢穴。阅读this Programming Ruby section以获得有关差异的详细解释。
作为另一种风格的挑剔,尽量不要在Ruby中使用if not ...
。这就是unless
的用途。但除非 - 否则也是糟糕的风格。我将args.each
块的内部重写为:
raise "already have attr_option for #{a}" if self.method_defined?(a)
define_method "#{a}" do
@options[:"#{a}"] ||= {}
end
define_method "#{a}=" do |v|
@options[:"#{a}"] = v
end