views:

97

answers:

1

Here's some simple code that, for each argument specified, will add specific get/set methods named after that argument. If you write attr_option :foo, :bar, then you will see #foo/foo= and #bar/bar= instance methods on 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

So far, so good. I want to write some RSpec tests to verify this code is actually doing what it's supposed to. But there's a problem! If I invoke attr_option :foo in one of the test methods, that method is now forever defined in Config. So a subsequent test will fail when it shouldn't, because foo is already defined:

  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

Is there a way I can give each test an anonymous "clone" of the Config class which is independent of the others?

+3  A: 

One very simple way to "clone" your Config class is to simply subclass it with an anonymous class:

c = Class.new Configurator::Config
c.attr_option :foo

d = Class.new Configurator::Config
d.attr_option :foo, :bar

This runs for me without error. This works because all instance variables and methods that get set are tied to the anonymous class instead of Configurator::Config.

The syntax Class.new Foo creates an anonymous class with Foo as a superclass.

Also, throwing an Exception in Ruby is incorrect; Exceptions are raised. throw is meant to be used like a goto, such as to break out of multiple nests. Read this Programming Ruby section for a good explanation on the differences.

As another style nitpick, try not to use if not ... in Ruby. That's what unless is for. But unless-else is poor style as well. I'd rewrite the inside of your args.each block as:

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
Mark Rushakoff
+1 for solid advice and a novel approach; I would not have thought of creating the anonymous classes and was pondering something like creating an anonymous module with the class and then destroying it. This is clearly a better solution.
John Feminella