We have updated the content of our program. To access the current Software Engineering curriculum visit curriculum.turing.edu.
Ruby Object Model
Learning Goals
- Develop a mental model for how Ruby manages instances, classes, superclasses, and modules
- Understand Ruby’s Lookup Chain for methods
Vocabulary
- Inheritance
- Superclass
- Module
- Object Model
- Look Up Chain
WarmUp
- What is class inheritance and how is it implemented?
- What is a module and how is it implemented?
- How do you know what variables, methods and classes you have available at any given time?
The Ruby Object Model
Investigative Methods
These three methods can help you investigate the relationships between classes and modules. All methods are run on the class (i.e. String
, Hash
)
.ancestors
: lists all classes along the inheritance chain, and any modules included by those classes. See docs..included_modules
: returns a list of all modules included by any class along the inheritance chain. See docs..superclass
: returns the superclass of the class. See docs.
Mapping Ruby’s Object Model
Superclasses
We are going to create a Dog
class. That Dog
class is going to inherit from another class called Animal
. That Animal
class is going to include 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.
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:
Chart Exercise
Break into small groups.
Using .class
, .ancestors
, .included_modules
, and .superclass
, diagram the Object Model of these several commonly-used Ruby classes: Hash, Array, String, Integer, and Float.
The Lookup Chain
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?
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 pointerInstances
: store instance variables, have a class pointerClasses
are also instances (of Class)Classes
can only inherit from one other class (its ‘superclass’)Classes
can include multiple Modules.Modules
can be mixed-in to multiple classes (mixins)
WrapUp
- How does Ruby’s look up 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
Additional Resources
- Additional Info
- Read Camilo Reyes’ “Understanding the Object Model.”
- Ruby Object Model Video