The Startup
Published in

The Startup

OOP 101

Superclass, Duperclass

Inheritance and other super things in Ruby

Photo by Wonderful Image Gallery

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.

Inheritance

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.

Class Inheritance

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.

The Dog and 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 Dog and Bird classes, we can move it to the Animal class since subclasses have access to any behaviours in their superclass.

When we instantiate a Dog or 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 @name in Animal to verify.

If we instantiate Dog or Bird objects now, we can call the name getter method on them.

Our Dog and Bird objects use the initialize method, @name instance variable, and name getter method as if they’re defined in the Dog and 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 Dog and 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 Bird's superclass Animal.

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!

Super Duper

Inside of the Dog class’s 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 Dog class’s name method, it would call the Dog class’s name method, which would call the Dog class’s name method, and so on and so forth until a SystemStackError is raised.

We haven’t entirely overridden the Animal class’s 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 Dog class’s 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 Dog class’s 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 Animal class’s initialize method in the Dog class’s 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 Dog object.

Hm, we didn’t intend for our object’s name to be '5 lbs' too!

But when we use super to call the Animal class’s initialize method, we forget to pass in the name argument it takes. There’s no ArgumentError because super automatically forwards the weight argument we passed into the Dog class’s 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: super().

Modules

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.

Mix-In Modules

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 Dog and Bat classes inherit from a Mammal class that inherits from the Animal class.

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 Dog and 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.

But 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 Bat class.

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 Bird and Bat classes with the reserved word include.

Now Bird and 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.

Namespace Modules

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.

Container Modules

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 self.

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 Bat before Animal. Interestingly, Ruby checks Echolocatable before 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.

Wait, 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 Object before Kernel before BasicObject. If it makes it all the way to BasicObject and still doesn’t find the method, it’ll raise a NoMethodError because BasicObject is the only class that doesn’t have a superclass and thereby the end of the method lookup path.

Boom. Hydrated.

Overriding Methods

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 Kernel module.

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.

By default, 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 Dog class.

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 Dog class’s to_s method!

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 Array class’s to_s method and turns the object into a String object.

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 to_s method.

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 Array class’s 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 Array class’s to_s method on the object’s name now, it calls the class’s join method on self. Recall self refers to the calling object inside of an instance method, in this case the object’s name. This turns it into a joined String object.

Altogether now!

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.

Almost There!

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:

Unlisted

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Rebecca Nguyen

Rebecca Nguyen

Software engineer @ GitHub. Co-creator @ Haven. Passionately curious. I like to read books, make ice cream, and learn things. Chinese-Vietnamese. She/her.