The Lookup Chain
Learning Goals
- Understand and explain the Lookup Chain and how it relates to the principle of Polymorphism in Object-Oriented Programming.
- Explore how Ruby’s object model facilitates this principle.
Vocabulary
- Inheritance
- Polymorphism
- Superclass
- Module
- Object Model
- Lookup Chain
WarmUp
- What is class inheritance and how is it implemented in Ruby?
- What is a module in Ruby and how is it implemented?
- How do you know what variables, methods, and classes you have available at any given time in Ruby?
The Ruby Object Model
Investigative Methods
These three methods can help you investigate the relationships between classes and modules in Ruby. All methods are run on the class (i.e., String.ancestors
, Hash.included_modules
):
.ancestors
: lists all classes along the inheritance chain, and any modules included by those classes..included_modules
: returns a list of all modules included by any class along the inheritance chain..superclass
: returns the superclass of the class.
Mapping Ruby’s Object Model
Superclasses
We are going to create a Dog
class that inherits from another class called Animal
, which includes a module called AnimalBehavior
:
# dog.rb
require './animal'
class Dog < Animal
end
dog = Dog.new
require 'pry'; binding.pry
# animal.rb
require './animal_behavior'
class Animal
include AnimalBehavior
end
# animal_behavior.rb
module AnimalBehavior
end
Notice that we haven’t actually included any methods in these classes/modules. We don’t need them to map Ruby’s Object Model.
When we run the dog.rb
file, we create a new instance of Dog
and then hit a pry. Using the investigative methods we defined above, we can learn about that dog instance’s ancestors:
pry(main)> dog
# => #<Dog:0x007ff414932eb0>
pry(main)> dog.class
# => Dog
pry(main)> Dog.ancestors
# => [Dog, Animal, AnimalBehavior, Object, PP::ObjectMixin, Kernel, BasicObject]
pry(main)> Dog.superclass
# => Animal
pry(main)> Animal.superclass
# => Object
pry(main)> Object.superclass
# => BasicObject
pry(main)> BasicObject.superclass
# => nil
Calling .class
on the Dog object leads us to the Dog
class. Calling .superclass
on the Dog
class leads us to Object
, and calling superclass
on Object
leads us to BasicObject
. BasicObject
has no superclass, so the inheritance chain ends there. We can summarize this information in a diagram:
Notice how we have included two instances of Dog in this diagram. This is to illustrate that there can be many instances of a class, and they all have a .class
pointer to their Class. In this example, there can be many instances of Dog that all have the same Dog
class.
Also notice that we call .superclass
on the Dog
class, not the dog instance. What happens if we call .superclass
on a dog instance? Try it to find out.
Because our class inherits from Object
, which inherits from BasicObject
, we know that any class we create will have those two ancestors. This is where :new
comes from. Look in the Ruby Docs BasicObject Page and you’ll see the :new
defined there.
Here is how you might implement a similar structure in JavaScript
class Animal {
}
class Dog extends Animal {
}
const dog = new Dog();
dog instanceof Animal; // true
Object.getPrototypeOf(Dog); // Animal
Mixins and Modules
We’ve mapped out the inheritance chain for our dog, but what about the modules? What happens if we call included_modules
on our Dog
class.
pry(main)> Dog.included_modules
#=> [AnimalBehavior, PP::ObjectMixin, Kernel]
We can see our AnimalBehavior
module (along with some others), but we included that in Animal
, not in Dog
, so why is it showing up here? included_modules
shows all modules that were included in any superclass, so it won’t tell us where that module was included. In this case, we get more information if we start at the top of the inheritance chain to figure out where modules first appear (you may get slightly different results depending on what Ruby version you are running):
pry(main)> BasicObject.included_modules
# => []
pry(main)> Object.included_modules
# => [PP::ObjectMixin, Kernel]
pry(main)> Animal.included_modules
# => [AnimalBehavior, PP::ObjectMixin, Kernel]
pry(main)> Dog.included_modules
# => [AnimalBehavior, PP::ObjectMixin, Kernel]
From this information, we can deduce that BasicObject
doesn’t include any modules, Object
includes PP:ObjectMixin
and Kernel
(you don’t need to worry about what those are), and Animal
includes AnimalBehavior
.
Our updated diagram:
Note
JavaScript does not have native support for mixins within its class system.
Chart Exercise
Using .class
, .ancestors
, .included_modules
, and .superclass
, diagram the Object Model of these several commonly-used Ruby classes: Hash, Array, String, Integer, and Float.
Feel free to collaborate with cohort mates!
The Lookup Chain and Polymorphism
In programming, polymorphism lets us use the same method or property in different classes or objects. Each class or object can have its own special version of a method through a process called method overriding, where a local definition of a method overrides the same method from the parent class.
Note
Another form of polymorphism is method overloading, which allows you to create multiple versions of the same method, each with different parameters. Contrary to method overriding, an overloaded method can live in the same class. Neither Ruby nor JavaScript support method overloading.
We now have a mental model for how Ruby manages classes, instances, superclasses, and modules, but why does it matter? The biggest implication of the Object Model is the Lookup Chain. We know that we can store methods in several places (class, superclass, module), but what is the exact order that Ruby looks for things? If a method is defined in several places, which one will Ruby use?
Understanding the Lookup Chain is crucial for mastering Polymorphism in Software Development. This chain determines the method resolution path when a method is called on an object and what should happen if that method is not found directly on the object.
Note
In JavaScript, the prototype chain plays a similar role to Ruby’s Lookup Chain, affecting how methods and properties are accessed and overridden.
Lookup Chain Exercise
Complete this activity. In the first part of the activity, you will map the Object Model for a Chair
instance, just as we did above. Then you will alter the code to explore the order of the Lookup Chain.
Once you have finished the activity, write out the order of the Lookup Chain as concisely as possible.
Other Definitions and Rules
Classes
- Store instance methods, have a
.superclass
pointer - Are also instances (of the
Class
class) - Can only directly inherit from one other class (its ‘superclass’)
- Can include multiple Modules.
- Store instance methods, have a
Instances
- Store instance variables, have a
.class
pointer
- Store instance variables, have a
Modules
- Can be mixed-in to multiple classes (mixins)
Wrap Up
- How does Ruby’s lookup chain work? What is the order it checks things?
- What are three methods you can use to learn about where a built-in Ruby method gets its components?
- Draw a diagram of where Ruby would look for the method
::new