Getting a list of classes that include a module
Asked Answered
A

4

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?

Ally answered 12/10, 2010 at 18:36 Comment(1)
have you tried this: #3527945Paripinnate
O
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.

Orphism answered 7/3, 2016 at 8:2 Comment(0)
C
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 }
Calci answered 17/8, 2021 at 19:8 Comment(0)
A
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.

Aden answered 16/3, 2011 at 0:5 Comment(1)
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.Ally
S
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
Schnapp answered 12/10, 2010 at 18:59 Comment(1)
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.Ally

© 2022 - 2024 — McMap. All rights reserved.