Schwern
Schwern

Reputation: 164809

How do I include behavior in a subclass?

I have a concern with an included block. In this example it sets a class instance variable.

require 'active_support/concern'

module Mod1
  extend ActiveSupport::Concern

  included do
    p "#{self}: included Mod1"
    @foo = "foo is set"
  end

  class_methods do
    def foo
      @foo
    end
  end
end

If I include it I get the method foo and the included variable is set.

class Parent
  include Mod1
end

p Parent.foo # "Parent: included Mod1" "foo is set"

But if I subclass from Parent, I inherit the method foo, but the included block is not run.

class Subclass < Parent
end

p Subclass.foo # nil

Even if I include the module, the included block is not run. I expect because Subclass.include?(Mod1) is true.

class Subclass < Parent
  include Mod1
end

p Subclass.foo # nil
p Subclass.include?(Mod1) # true

How do I write a concern such that its included block runs even on subclasses?

Upvotes: 3

Views: 1213

Answers (2)

max
max

Reputation: 102036

ActiveSupport::Concern#included is really just syntactic sugar for Module#included so it only ever fires when the module is included in the parent class. The hook you are looking for is Class#inherited which is invoked whenever a subclass of the current class is created.

require 'active_support/concern'

module Mod1
  extend ActiveSupport::Concern

  included do
    set_foo!
  end

  class_methods do
    def foo
      @foo
    end

    def set_foo!
      p "Setting foo"
      @foo = "foo is set"
    end

    def inherited(child_class)
      puts "inherited!"
      child_class.set_foo!
    end
  end
end

class Parent
  include Mod1
end

class Subclass < Parent; end
require 'minitest/autorun'
class Mod1Test < Minitest::Test
  def test_class_ivar_set_in_subclass
    assert_equal("foo is set", Subclass.foo) # passes
  end
end
"Setting foo"
inherited!
"Setting foo"
Run options: --seed 35149

# Running:

.

Finished in 0.000999s, 1000.7185 runs/s, 1000.7185 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Upvotes: 4

Huy Ha
Huy Ha

Reputation: 179

It's funny here. Actually the SubClass has the method foo, but the return value @foo is not set. The SubClass inherits methods from Parent, but as you are using @foo which is a variable so it cannot be shared between 2 object Parent and SubClass. You can check by getting the source location of the method foo in SubClass

SubClass.method(:foo).source_location

And if you move the logic to the method foo, you can see that SubClass will return the expected value. For example:

require 'active_support/concern'

module Mod1
  extend ActiveSupport::Concern

  included do
  end

  class_methods do
    def foo
      p "#{self}: included Mod1"
      "foo is set"
    end
  end
end

Then calling foo from SubClass will return foo is set

The included do in active_support/concern is usually used to declare Rails features such as relationships or validations or scopes.

https://api.rubyonrails.org/classes/ActiveSupport/Concern.html

Upvotes: 0

Related Questions