In studying the inheritance of methods I came across what I consider a surprising behavior of modules included into classes.
First, when you include a module into a class, the module methods are available in the class and they also propagate down to subclasses. This is reflected in the fact that the module is added to the ancestors of both the including class and all subclasses (be the defined before or after the include).
class LateIntoClassTest < Test::Unit::TestCase class A end class B < A end module LateInClass end class A include LateInClass end class C < A end def test_including_a_module_into_a_superclass_adds_to_ancestors # LateInClass is added to A assert_equal [A, LateInClass, Object, Kernel], A.ancestors # LateInClass is added to B assert_equal [B, A, LateInClass, Object, Kernel], B.ancestors # LateInClass is added to C assert_equal [C, A, LateInClass, Object, Kernel], C.ancestors end end
What is surprising (at least to me), is that the same is not true when you include a module into an included module. I thought modules were kind of like superclasses; if you add to a module then you add to everything that uses the module. Not so.
Here you can see LateInModule is not added to classes that already include A. By contrast, classes defined after the ‘late’ include will add LateInModule to their ancestors.
class LateIntoModuleTest < Test::Unit::TestCase module A end class B include A end module LateInModule end module A include LateInModule end class C include A end def test_including_into_an_included_module_DOES_NOT_add_to_ancestors # LateInModule is added to A assert_equal [A, LateInModule], A.ancestors # LateInModule is missing from B assert_equal [B, A, Object, Kernel], B.ancestors # LateInModule is added to C assert_equal [C, A, LateInModule, Object, Kernel], C.ancestors end end
You might take from this the lesson that modules are not superclasses, they are collections of methods poured into a class by include. But that isn’t the full story either. After all, you can modify a module and still have those changes propagate into an including class.
class LateModuleModificationTest < Test::Unit::TestCase module A end class B include A end module A def late_method; true; end end def test_included_modules_MAY_be_modified assert_equal true, B.new.late_method end end
I find it tricky to express this behavior descriptively, even though the cause is clear; modules only add to ancestors when first included in a class.
Update: for those who are interested, Redmine has a feature request regarding how modules get added to ancestors.