Zack
Zack

Reputation: 6606

Why use extend/include instead of simply defining method in main object?

RSpec adds a "describe" method do the top-level namespace. However, instead of simply defining the method outside of any classes/modules, they do this:

# code from rspec-core/lib/rspec/core/dsl.rb 
module RSpec
  module Core
    # Adds the `describe` method to the top-level namespace.
    module DSL
      def describe(*args, &example_group_block)
        RSpec::Core::ExampleGroup.describe(*args, &example_group_block).register
      end
    end
  end
end

extend RSpec::Core::DSL
Module.send(:include, RSpec::Core::DSL)

What is the benefit of using this technique as opposed to simply defining describe outside any modules and classes? (From what I can tell, the DSL module isn't used anywhere else in rspec-core.)

Upvotes: 1

Views: 477

Answers (2)

Myron Marston
Myron Marston

Reputation: 21830

I made this change a few months ago so that describe is no longer added to every object in the system. If you defined it at the top level:

def describe(*args)
end

...then every object in the system would have a private describe method. RSpec does not own every object in the system and should not be adding describe willy-nilly to every object. We only want the describe method available in two scopes:

describe MyClass do
end

(at the top-level, off of the main object)

module MyModule
  describe MyClass do
  end
end

(off of any module, so you nest your describes in a module scope)

Putting it in a module makes it easy to extend onto the main object (to add it to only that object, and not every object) and include it in Module (to add it to all modules).

Upvotes: 4

Mon ouïe
Mon ouïe

Reputation: 958

Actually, if that's all there is in the code, I don't really believe it to be much better — if at all. A common argument is that you can easily check that RSpec is responsible for addinng this method in the global namespace by checking the method owner. Somehow it never felt this was needed, as the location of the method already stores that information.

Defining the method outside of any scope would have be equivalent to defining a private instance method in Object:

class Object
  private
  def double(arg)
    arg * 2
  end
end

double(3)      # OK
3.double(3)    # Error: double is private
self.double(3) # Error: double is private

I think privateness is a useful aspect, because it prevents from making certain method calls that have no meaning, that the code shown in the question lacks.

There's an advantge to defining the method in a module, though, but the RSpec code doesn't seem to make use of it: using module_function, not only do you preserve privateness of the instance method, but you also get a public class method. This means that if you have an instance method of the same name, you will still be able to refer to the one defined by the module, by using the class method version.

A common example of module_function is the Kernel module, which contains most function-like core methods like puts (another one is Math). If you're in a class that redefines puts, you can still use Kernel#puts explicitly if you need:

class LikeAnIO
  def puts(string)
    @output << string
  end

  def do_work
    puts "foo" # inserts "foo" in @output
    Kernel.puts "foo" # inserts "foo" in $stdout
  end
end

Upvotes: 1

Related Questions