Today I learned that Ruby modules are actually implemented as classes under the hood.
When you include
a module (which is kind of a class in disguise), it actually gets dynamically inserted into the inheritance tree.
Consider the example of a school. We might have a Person class to model every person in the school with some common attributes:
class Person
attr_accessor :given_name
attr_accessor :surname
end
Then we might have subclasses for students, staff and parents to model attributes specific to each group. Focusing on the student, our program might look like this:
class Person
attr_accessor :given_name
attr_accessor :surname
end
class Student < Person
attr_accessor :year_level
end
To add attributes to staff and students that don’t apply to parents we could define a module and mix it in. Let’s say we want to be able to add students (e.g. Harry and Ron) and staff (e.g. Minerva McGonagall) to a house (e.g. Gryffindor). Our program now looks more like this:
class Person
attr_accessor :given_name
attr_accessor :surname
end
module House
attr_accessor :name
attr_accessor :colour
end
class Student < Person
include House
attr_accessor :year_level
end
Previously Student inherited directly from Person but now the House module (or, more accurately, a copy of it) gets inserted as the new parent class of Student. Under the hood the inheritance hierarchy now looks more like Student < House < Person
.
That’s all well and good but Ruby doesn’t support multiple inheritance; each class can only have one parent. So how does including multiple modules work? It turns out, they each get dynamically inserted into the inheritance tree, one after another.
Stretching our example a little further, if we had some attributes common to parents and students, we might use another module. Perhaps we want to record the household so we can model that Ron, Ginny and Mr & Mrs Weasley are all part of the “Weasley” household (I’m stretching this example quite far, hopefully it doesn’t snap and take out an eye). Our student class might now look like this:
class Person
attr_accessor :given_name
attr_accessor :surname
end
module House
attr_accessor :house_name
attr_accessor :house_colour
end
module Household
attr_accessor :household_name
attr_accessor :address
end
class Student < Person
include House
include Household
attr_accessor :year_level
end
Under the hood, Ruby first inserts House as the new parent (superclass) of Student but then on the very next line it inserts Household as the parent of Student. The inheritance chain is now Student < Household < House < Person
.