27

I have a mixin for which I would like to get a list of all the classes that have included it. In the mixin module, I did the following:

module MyModule
  def self.included(base)
    @classes ||= []
    @classes << base.name
  end

  def self.classes
    @classes
  end
end

class MyClass
  include MyModule
end

This works pretty well:

> MyModule.classes #=> nil
> MyClass.new #=> #<MyClass ...>
> MyModule.classes #=> ["MyClass"]

Now, I would like to extract this part out into a separate module that can be included in my other mixins. So, I came up with the following:

module ListIncludedClasses
  def self.included(base)
    p "...adding #{base.name} to #{self.name}.classes"

    @classes ||= []
    @classes << base.name

    base.extend(ClassMethods)
  end

  def self.classes
    @classes
  end

  module ClassMethods
    def included(module_base)
      p "...adding #{module_base.name} to #{self.name}.classes"

      @module_classes ||= []
      @module_classes << module_base.name
      super(module_base)
    end
    def classes
      @module_classes
    end
  end

end

module MyModule
  include ListIncludedClasses
end

This doesn't work though, because the #included(module_base) method being added to MyModule from ListIncludedClasses is never getting run. Interestingly enough, it does successfully add #classes to MyModule.

> MyModule.classes #=> 
  "...adding Rateable to ListIncludedClasses.classes"
  => nil 
> ListIncludedClasses #=> ["MyModule"]
> MyClass.new #=> #<MyClass ...>
# ^^ THIS SHOULD BE ADDING MyClass TO MyModule.classes ^^
> MyModule.classes #=> nil

What am I missing?

jangosteve
  • 1,562
  • 2
  • 14
  • 26
  • have you tried this: http://stackoverflow.com/questions/3527445/when-are-modules-included-in-a-ruby-class-running-in-rails – rickypai Jan 13 '13 at 11:00

4 Answers4

39
module MyMod; end

class A; include MyMod; end
class B < A; end
class C; end

ObjectSpace.each_object(Class).select { |c| c.included_modules.include? MyMod }
  #=> [B, A]

See ObjectSpace:: each_object.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
3

One caveat to Cary's answer (which is great!) is that it will only pick up on classes that have already been evaluated by the VM.

So, if you're running that code in a setting like a development Rails console you'll need to explicitly require the files you're interested in before checking if the module has been included.

You can do that like this:

Dir[Rails.root.join("app/models/**/*.rb")].each { |f| require f }
2

Actually, your module extension module works. The problem is in your test: when you created a random unnamed class with Class.new, you forgot to include MyModule. As a side note, you can take your read-only accessor for classes that include the module and use the helpful Module#attr_reader method.

Aaa
  • 1,854
  • 12
  • 18
  • Thanks for the reply. Yes, it works when I try it now, I'm not sure what I was doing at the time. I think I had it in a Rails project instead of isolating it, so who knows. It wasn't that I forgot the `MyClass` definition with `include`, I just didn't re-type it in the example above. Otherwise, MyClass.new would have thrown `uninitialized constant MyClass`. Also `Module#attr_reader` creates instance methods, so yeah I could have used it inside `ClassMethods`, and then also inside a `class << self` block in `ListIncludedClasses`. It's more efficient, so in production that'd be the way to go. – jangosteve Mar 16 '11 at 04:32
1

You probably should use extend instead of include since former adds class level methods, while latter - instance level methods (why you have access to @classes).

Try this:

module MyModule
  extend ListIncludedClasses::ClassMethods
end
Eimantas
  • 48,927
  • 17
  • 132
  • 168
  • 2
    I don't know if you missed it, but extend(ClassMethods) is in the self.included(base) method, so it is extending the class methods. There are other instance methods that need to be added; this is the popularly accepted technique to both including the instance methods and extending the class methods. Also, directly extending the class methods, as you suggested, makes no difference. – jangosteve Oct 12 '10 at 22:03