Inheritance
To understand Ruby’s inheritance model, you need to know how it performs method name lookup. This explanation comes from The Ruby Programming Language by David Flanagan and Yukihiro Matsumoto (the author of Ruby itself):
When Ruby evaluates a method invocation expression, it must first figure out which method is to be invoked. The process for doing this is called method lookup or method name resolution. For the method invocation expression
o.m
, Ruby performs name resolution with the following steps:
- First, it checks the eigenclass of
o
for singleton methods namedm
.- If no method
m
is found in the eigenclass, Ruby searches the class ofo
for an instance method namedm
.- If no method
m
is found in the class, Ruby searches the instance methods of any modules included by the class ofo
. If that class includes more than one module, then they are searched in the reverse of the order in which they were included. That is, the most recently included module is searched first.- If no instance method
m
is found in the class ofo
or in its modules, then the search moves up the inheritance hierarchy to the superclass. Steps 2 and 3 are repeated for each class in the inheritance hierarchy until each ancestor class and its included modules have been searched.
Sound confusing? Let’s break it down with a few examples.
Parent-child class inheritance
We’ll start with class-based inheritance, as this is the most widely
understood. If class Child
inherits from class Parent
, Child
’s methods
can call methods from Parent
using callSuper()
. Child
inherits all the
methods of Parent
and can override them if necessary:
var Parent = new Class({ speak: function() { return "I'm an object"; }, writeCode: function(code) { return "I wrote: " + code; } }); var Child = new Class(Parent, { speak: function() { return this.callSuper().replace(/[aeiou]/ig, '_'); } }); (new Child).speak() // -> "_'m _n _bj_ct" (new Child).writeCode('function(){}') // -> "I wrote: function(){}"
This style of inheritance is commonly supported by object-oriented languages
and is pretty simple to understand: a class can only have one parent class,
and can call methods from it. It is worth mentioning here that, in common with
Ruby, jsclass
makes arguments to callSuper()
optional. By default, it
passes the same parameters to the parent method as were used to call the child
method. More information can be found under Creating classes.
Inheritance from mixins
This area causes some confusion as it relates to multiple inheritance, but Ruby’s method lookup rules do give predictable dependable results. When you mix a module into a class, it becomes part of the inheritance tree. Ruby searches all the modules included by a class before searching the parent class, in reverse inclusion order. Also, it’s a depth-first search. If a module includes other modules, these are searched before moving on. An example should clarify things:
var ModA = new Module({ speak: function() { return "speak() in ModA"; } }); var ModB = new Module({ speak: function() { return this.callSuper() + ", speak() in ModB"; } }); var ModC = new Module({ include: ModB, speak: function() { return this.callSuper() + ", and in ModC"; } }); var Foo = new Class({ include: [ModA, ModC], speak: function() { return this.callSuper() + ", and in class Foo"; } });
Suppose we create a new Foo
and call speak()
on it. Foo
includes ModA
and ModC
, which itself includes ModB
. So when looking for methods, we look
in Foo
, then ModC
, then ModB
, then ModA
. At each stage, callSuper()
forces the search to continue along the inheritance chain.
(new Foo).speak() // -> "speak() in ModA, speak() in ModB, and in ModC, and in class Foo"
This nicely demonstrates the late binding of callSuper
. ModB
does not
include any other modules, so its call to callSuper
depends on the module
ModB
is being called in, and which other modules are part of that
inheritance chain.
Remember that all the modules included by a class are searched before moving on to the parent class when searching for methods.
Late binding
The inheritance chain of a class can be modified at any time by mixing more modules into it. Consider this extension of the above example:
var ModD = new Module({ speak: function() { return 'ModD speaks'; } }); Foo.include(ModD);
The lookup chain for Foo
now goes Foo
, ModD
, ModC
, ModB
, ModA
.
speak()
in ModD
does not callSuper()
, so methods in ModC
upwards are
no longer called.
(new Foo).speak() // -> "ModD speaks, and in class Foo"
Singleton methods
When you create an object from a class, the object does not have any custom methods of its own. It only has methods inherited from its class.
var Machine = new Class({ run: function() { return 'Machine is running'; } }); var obj = new Machine(); obj.run() // -> "Machine is running"
But, we can extend objects with their own methods. Every object has an
eigenclass (also referred to as a metaclass in some literature), and this
eigenclass stores methods you add to the object. Methods tied to an object
(rather than defined in a class or a module) are known as singleton methods,
as they’re defined on a single object. This class is the first place
jsclass
looks when you call a method on an object.
obj.extend({ run: function() { return this.callSuper() + ', and obj is running'; } });
Where does callSuper
lead to? After we look in the object’s eigenclass, we
look in the class the object belongs to, which in this case is the Machine
class. So we get:
obj.run() // -> "Machine is running, and obj is running"
But remember that we always search the modules included by a class before moving onto its parent class. So if we extend an object using a module, that module gets included into the object’s eigenclass and comes between the object and its class:
var MachineExtension = new Module({ run: function() { return this.callSuper().toUpperCase(); } }); obj.extend(MachineExtension);
It’s not immediately apparent what callSuper()
does in this case:
MachineExtension
has no parent class and does not include any other modules.
Because Ruby’s method lookup is late-bound, the meaning of callSuper()
in
MachineExtension
depends on the context you use it in. You should think of
that last line as being equivalent to:
obj.__eigen__().include(MachineExtension);
So obj
’s eigenclass includes MachineExtension
, making it part of the
inheritance tree. When we call obj.run()
now, the call stack will find
obj.run
, MachineExtension#run
and Machine#run
in that order.
Machine#run
returns "Machine is running"
, MachineExtension#run
turns
that to uppercase, and obj.run
appends ", and obj is running"
.
obj.run() // -> "MACHINE IS RUNNING, and obj is running"
So it couldn’t be simpler! This is fairly advanced Ruby functionality, but it will stick if you remember the following: to find a method, you look in the object’s eigenclass, then in the class the object belongs to, then that class’ parent class, and so on. At each level, you search all the modules included by the class before moving onto the parent.