Inheritance and other super things in Ruby
This is the third article in a four-part series on the basics of object-oriented programming in Ruby. Read the previous article here.
No class is an island entire of itself. We’ve only dabbled on the shores of inheritance so far, but it’s core to how we think about and design the interaction of our classes. If the classes are islands, inheritance is like the waters that distinguish yet tie them together. Let’s dive in head first.
In the first article of this series, we said inheritance allows us to extract common behaviours from specialized classes to a base class or form specialized classes from a base class that already exists. Not only do we build more tidy and versatile programs this way, we’re able to model our classes after the real world and natural hierarchies in order to think about objects, relationships, and behaviours at a higher level of abstraction.
So far, we’ve only defined a
Dog class. As we define more, we can start to consider the natural hierarchy of what our classes represent. Let’s reset the
Dog class and initialize one state for now.
We’ll add a
Bird class too.
Now we have a
Dog class and a
Bird class. Since dogs and birds are both animals, it’d make sense for our classes to inherit from an
Animal class. In Ruby, we use the
< symbol to define inheritance.
Bird classes are now specialized classes, or subclasses, of the
Animal base class, or superclass. Seeing as there’s already a common behaviour in our
Bird classes, we can move it to the
Animal class since subclasses have access to any behaviours in their superclass.
When we instantiate a
Bird object, the
initialize method in the
Animal class is called and the
@name instance variable is initialized. Let’s add a getter method for
Animal to verify.
If we instantiate
Bird objects now, we can call the
name getter method on them.
Bird objects use the
@name instance variable, and
name getter method as if they’re defined in the
Bird classes themselves. By only defining them once in their superclass, though, we keep our code DRY and save memory space.
But what happens if we define a
name method in the
Dog class too?
Let’s call the
name method on our
Bird objects again.
Class inheritance is like the apple falling from the tree. While a child inherits their parent’s behaviours, they can outgrow them and flourish into their own person. Our
Dog object initially inherits the
name method from the
Animal class, but we override it by redefining one in the
Dog class. When we call an instance method on an object, Ruby checks the object’s class for the method before it looks in the superclass. The same goes for when we call a class method on a class. This is the method lookup path.
After we call
name on our
Dog object, Ruby checks for the method in the
Dog class and finds it. But when we call
name on our
Bird object, Ruby checks for the method in the
Bird class and doesn’t find it. So, Ruby continues to look for it in
In either case, Ruby runs the
name method upon finding it and doesn’t look any further. Where does it look, though, if it doesn’t find the method in the object’s class or superclass? We’ll come back to this soon!
Inside of the
name method, we reference the
@name instance variable directly rather than the inherited
name getter method. That’s because we can’t access it when we call
name on a
Dog object, since Ruby finds a method by that name in the
Dog class and won’t look any further. If we tried to call
name inside of the
name method, it would call the
name method, which would call the
name method, and so on and so forth until a
SystemStackError is raised.
We haven’t entirely overridden the
name method, though. We can use the
super reserved word inside of a method to traverse the method lookup path for a method of the same name. If we use
super in the
name method, Ruby jumps to the superclass and looks for a
name method. It finds one, allowing us to retrieve our object’s name.
Let’s try a more common use case for
super. We’ll remove the
name method (sorry, good boys!) and redefine an
initialize method. We’ll add another state and a getter method for it.
In a sense, we extend the
initialize method in the
initialize method. We use
super to access the superclass’s method and initialize a
@name instance variable, then we initialize a
@weight instance variable back in the subclass’s method. So,
Dog objects have a name state and a weight state, while
Bird objects continue to only have a name state.
Seeing as we handle a
weight argument in the
Dog class, we’ll pass in
'5 lbs' when we instantiate a
Hm, we didn’t intend for our object’s name to be
'5 lbs' too!
But when we use
super to call the
initialize method, we forget to pass in the
name argument it takes. There’s no
super automatically forwards the
weight argument we passed into the
initialize method. If we pass in multiple arguments,
super forwards all of them, even if the
Animal class’s method takes more or less arguments than the
Dog class’s. Unless we intend for this, we have to specify the arguments we want to forward.
If a subclass’s method takes arguments but a superclass’s method doesn’t, we can use
super like this to specify we don’t want to forward any arguments:
Although class inheritance enables us to remove duplication and think at a higher level of abstraction, not every class and behaviour fits into a hierarchy or is purely hierarchical. Modules are similar to classes in that they’re like building blocks or containers. However, we use them to interact with and support our classes rather than instantiate objects, and in varied ways to achieve distinct goals and provide semantic order to our programs.
A class can only explicitly inherit from one superclass in Ruby, but we can mimic multiple inheritance with mix-ins.
To demonstrate, we’ll rearrange our inheritance hierarchy and add a
Bat class. Dogs and bats are both mammals, so we’ll have the
Bat classes inherit from a
Mammal class that inherits from the
If we pretend all
Bird objects are able to fly, maybe we’ll define a
fly method in the
Bird class. All mammals have hair or fur, so perhaps we’ll come up with a method related to that in the
Mammal class. We know
Bat objects have access to methods in the
Mammal class but not in the
Bird class or each other’s class. This makes sense so far.
Bat objects should be able to fly too. We can’t move the
fly method from the
Bird class to the
Animal class, though, because
Dog objects aren’t able to fly. (Unless...) And we’d repeat ourselves if we created another
fly method in the
A behaviour like flying seems better grouped in a module and made available to classes that require it rather than existing in a hierarchy.
We can define a
Flyable module with the reserved word
module and mix it in the
Bat classes with the reserved word
Bat objects are able to fly but
Dog objects aren’t. (Sorry again, good boys!)
Typically, classes are represented by nouns and mix-in modules are represented by verbs, but it can become confusing when to inherit a behaviour versus mix it in since modules act like a specialized implementation of multiple inheritance in which only an interface is inherited. In our classes, we can mix in as many as we’d like!
A good rule of thumb is to use class inheritance for is-a relationships and mix-in modules for has-a relationships. A bat is a mammal and has an ability to fly. It inherits the behaviours common to mammals and shares flight-related behaviours with birds. It can also have behaviours unique to itself.
Say our program deals with baseball too. We want to create classes for baseball equipment, but we already have a
Bat class for the mammal bat.
In addition to mix-ins, we can use modules to namespace. That is, to group related classes in order to encapsulate them and prevent name collisions. This also better organizes and makes clear their purposes.
We can access each
Bat class by explicitly referencing the module they’re in with the scope resolution operator, a double colon.
There’s no longer confusion about what kind of bat we want to use.
Finally, we can use modules to encapsulate related methods or those out of place in our program. Such methods are module methods and prepended with
Recall in a class method,
self refers to the class. Similarly, in a module method,
self refers to the module. That means
self.random_method is the same as
Random.random_method. We can call it directly on the module or with the scope resolution operator.
All modules act like a container of some kind. But if classes are blueprints and objects are houses, mix-in modules are moving trucks of furniture for the houses, namespace modules are folders for the blueprints, and container modules are storage units for everything else under the sun.
No Class is an Island
Now that we have a mental model of inheritance, we can start to see how object-oriented programs are the interaction of many specific but interrelated classes. Like we said earlier, no class is an island entire of itself, even if it’s the only one we define.
Method Lookup Path
At this point, we know the method lookup path gives precedence to a calling object’s class (or a calling class) over the superclass. We also know once a method is found, Ruby runs it and doesn’t look any further. But we’ve yet to see where it looks if it doesn’t find the method in the subclass or the superclass, or if we mix in modules.
Let’s clear our program.
We define a new
Bat class that inherits from an
Animal class. Since two modules are mixed in it and one is mixed in its superclass, it has access to every behaviour we add to any of our classes or modules. We can retrieve the method lookup path for it by calling the
ancestors class method.
(Hydration game: take a shot of water every time we say checks!)
It’s no surprise if we call a method on
Bat or its objects, Ruby checks
Bat before its superclass
Animal. But now, it also checks the modules mixed in
Animal. Interestingly, Ruby checks
Flyable although we mixed in
Flyable first. That means if we mix multiple modules in a class, Ruby checks them in the order opposite to that we mix them in.
After Ruby checks the modules, it checks
Animal and a pattern repeats. It checks the class, then the modules mixed in it, then its superclass.
Animal has a superclass? Isn’t it the superclass?
While a class can only explicitly inherit from one superclass in Ruby, every class implicitly inherits from the
Object class. The
Object class has a
Kernel module mixed in and inherits from the
BasicObject class. That’s the superclass. In other words, it’s the superclass of all classes!
That explains why Ruby checks
BasicObject. If it makes it all the way to
BasicObject and still doesn’t find the method, it’ll raise a
BasicObject is the only class that doesn’t have a superclass and thereby the end of the method lookup path.
Since every class inherits from
Object, all objects have access to the methods defined or mixed in
Object (and are technically polymorphic!). In some of our code examples, we call a
puts method here and there without having to define it because it’s made available by the
Just like we can override our custom class methods, we can override any
Object methods. But not so fast! We should only do this if we’re very intent and cautious, not because, say, we want to name a method a particular way.
A relatively safe and common method to override is
to_s. Let’s output a
Dog object by calling the
puts method on it, which will call the
to_s method on it in return.
to_s returns the name of the object’s class and an encoding of its object ID. This changes each time we run the program because we instantiate a new object. If we want a high-level string representation of the object instead, we can redefine a
to_s method in the
In the previous article, we mentioned an object’s name could be represented as any data type in our example. This is because so far as our methods were concerned, they automatically called a
to_s method on the object’s name.
Similarly, if we use an array of characters for our object’s name instead of a string here, we can still output a high-level string representation of the object. The
name getter method in our
to_s method will return an
Array object, but being interpolated in a string, another
to_s method will be called on it. That
to_s method is not to be confused with our
Many built-in classes override the default
to_s method like we do. When
to_s is called on an
Array object, it refers to the
to_s method and turns the object into a
Our object’s name can be represented as any data type in the
Dog class if it has access to another
to_s method that returns a string. We don’t have to change how we implemented the class or our
We may want to use an array for the object’s name for whatever reason, but say when we output the object, we want its name to be a joined string. We can override the
to_s method too!
Since we don’t inherit from the
Array class, we have to redefine the method in the class itself. (This is dangerous and only for demonstration purposes!)
When we call
to_s method on the object’s name now, it calls the class’s
join method on
self refers to the calling object inside of an instance method, in this case the object’s name. This turns it into a joined
This may seem like a minor feat, but in Ruby, we inherit a myriad of methods in both custom and built-in classes. Understanding how to leverage critical ones like
to_s can be of value in our programs.
There’s more ground to cover, but for the sake of length, we’ll continue our discussion in the next article. There, we’ll talk about more nuanced concepts related to inheritance: