Topics Covered in This Ruby on Rails Tutorial:
Inheritance, Overriding a Parent Class's Method, Calling a Parent Class's Method Using Super, Mixins & Modules
Exercise Overview
Time for a strategic Ruby deep-dive!
Now that you've gained practical experience building a real Rails application, it's crucial to step back and master some of Ruby's more sophisticated—yet fundamental—language features. These concepts form the backbone of robust, maintainable Rails applications that scale effectively in production environments.
In this comprehensive exercise, you'll explore how Ruby facilitates code reuse and organization through inheritance, mixins, and modules. These patterns are essential for building the kind of clean, DRY (Don't Repeat Yourself) codebases that modern development teams demand.
This tutorial uses Interactive Ruby (IRB) for hands-on practice. You'll create real classes and see immediate results as you explore advanced Ruby concepts.
Inheritance
Inheritance represents one of object-oriented programming's core principles, enabling objects to inherit both properties and methods from parent classes. This mechanism reduces code duplication while establishing clear hierarchical relationships between related concepts—a pattern you'll encounter throughout Rails' own architecture.
Launch Interactive Ruby by opening Terminal and executing the
irbcommand. IRB provides an ideal environment for experimenting with Ruby concepts in real-time.Let's establish our foundation by creating a
Petclass. This will serve as our parent class, demonstrating how common behaviors can be shared across related objects:class Pet attr_accessor :name def initialize(name) @name = name endNow define the
speakmethod, which establishes default behavior that child classes can inherit or override:def speak "Feed me!" endInstantiate your first Pet object to see inheritance in action:
fluffy = Pet.new("Miss Fluffy")Verify that our object correctly stores its name:
fluffy.nameTest the speak method:
fluffy.speakTerminal returns the universal pet declaration:
"Feed me!"Here's where inheritance demonstrates its power. We'll create a specialized Dog class that inherits all Pet capabilities while maintaining its distinct identity. The less-than symbol (
<) establishes the inheritance relationship—think of it as an arrow pointing from child to parent:class Dog < Pet endAt this point, Pet becomes our superclass (or parent class), while Dog serves as the child class inheriting all Pet behaviors.
Thanks to inheritance, our Dog class automatically possesses all Pet methods without any additional code. Let's verify this inheritance works seamlessly:
fido = Dog.new("Mr. Fido") fido.speakTerminal confirms inheritance is working by returning:
"Feed me!"While inheritance provides default behaviors, method overriding allows child classes to customize inherited functionality. This demonstrates polymorphism—different classes responding to the same method call in their own unique way:
class Dog < Pet def speak "Arf! Arf!" end fido.speakTerminal now returns
"Arf! Arf!"instead of the generic"Feed me!". Our Dog class has successfully overridden the parent's speak method while maintaining all other inherited behaviors.Let's confirm that other inherited methods remain intact:
fido.nameTerminal displays
"Mr. Fido". The Dog class seamlessly inherited theattr_accessor :namefunctionality from Pet, demonstrating how inheritance promotes code reuse.Create another child class to further illustrate inheritance flexibility:
class Reptile < Pet endTest our new Reptile class to confirm it inherits the same Pet behaviors:
sandra = Reptile.new("Mrs. Sandra") sandra.name sandra.speakSandra demonstrates perfect inheritance: she knows her name and speaks the default Pet phrase
"Feed me!". This showcases how multiple child classes can inherit from the same parent while maintaining their independence.Sometimes you want to extend rather than replace inherited functionality. Ruby's
superkeyword calls the parent class method, allowing you to build upon existing behavior rather than completely overriding it. This technique is particularly valuable when you need to add logging, validation, or additional processing to inherited methods:class Fish < Pet def speak super + " (bubble)" endObserve how
superenables method extension:sunny = Fish.new("Mr. Sunny") sunny.speakTerminal returns
"Feed me! (bubble)". Thesupercommand retrieved the parent's speak method result and allowed us to append Fish-specific behavior. This pattern is extremely common in Rails applications, where you often want to extend framework functionality rather than replace it entirely.
Building Your First Inheritance Hierarchy
Create Parent Class
Define a Pet class with attr_accessor :name, initialize method, and a speak method that returns 'Feed me!'
Create Child Class
Use the < symbol to create a Dog class that inherits from Pet: class Dog < Pet
Override Methods
Redefine the speak method in Dog class to return 'Arf! Arf!' instead of the parent's 'Feed me!'
Use Super Method
Create Fish class and use super to extend parent method: super + ' (bubble)' returns 'Feed me! (bubble)'
Method Override vs Super
| Feature | Override | Super |
|---|---|---|
| Result | Replaces parent method completely | Extends parent method functionality |
| Code Example | def speak 'Arf! Arf!' end | def speak super + ' (bubble)' end |
| Output | Arf! Arf! | Feed me! (bubble) |
| Use Case | Complete behavior change | Behavior enhancement |
Mixins
While inheritance provides a powerful mechanism for sharing code, Ruby's mixin system offers even greater flexibility through modules. Unlike single inheritance languages, Ruby's mixins allow you to compose functionality from multiple sources, creating more flexible and maintainable code architectures. This approach aligns perfectly with Rails' philosophy of convention over configuration and helps avoid the limitations of single inheritance hierarchies.
Let's create a module that can be shared across different types of objects. Modules encapsulate related functionality that can be mixed into multiple classes, regardless of their inheritance hierarchy:
module License def register(state) @name + " has been registered in " + state endWe've defined a License module containing registration functionality, but modules can't be instantiated directly—they must be included in classes to become useful.
Now we'll demonstrate the power of mixins by extending our existing Pet class with License functionality:
class Pet include License endThe
includestatement performs the magic of mixins, making all License methods available to Pet instances and, through inheritance, to all Pet subclasses. This demonstrates composition over inheritance—a key principle in modern software design.Mixins truly shine when you need to share functionality between unrelated classes. Let's create a Car class that has nothing to do with pets but still needs licensing capability:
class Car include License def initialize(model, owner) @name = owner + "'s " + model endNotice how we've defined an
initializemethod that accepts two parameters, demonstrating Ruby's flexibility in constructor design. The Car class can now access License functionality without any inheritance relationship to Pet.Let's test our mixin in action by registering Fido through the inherited License functionality:
fido.register("Oklahoma")Terminal returns
"Mr. Fido has been registered in Oklahoma". This works because Fido (Dog class) inherits from Pet, and Pet includes the License module. The functionality flows seamlessly through the inheritance chain, demonstrating how mixins and inheritance work together harmoniously.Now let's prove that mixins work across completely different class hierarchies:
car = Car.new("BMW", "Grandpa") car.register("Wyoming")Success! Terminal returns
"Grandpa's BMW has been registered in Wyoming". This demonstrates mixins' core value proposition: sharing functionality between unrelated classes without forcing artificial inheritance relationships or duplicating code.This exemplifies one of programming's most important principles: DRY (Don't Repeat Yourself). Mixins and modules are essential tools for maintaining DRY codebases, especially in large Rails applications where similar functionality appears across different domain models.
Don't Repeat YourselfMixins vs Traditional Inheritance
Real-World Mixin Example
License Module
Created once with register method that works for both pets and cars. Demonstrates code reusability across different object types.
Pet Class Integration
Extended Pet class using 'include License' statement. All Pet subclasses automatically gain registration capability.
Car Class Integration
Car class includes same License module despite being unrelated to Pet hierarchy. Shows mixin flexibility.
The Is_a? & Respond_to? Methods
Ruby provides powerful introspection capabilities that allow your code to make intelligent decisions at runtime. These methods are particularly valuable in Rails applications where you often need to handle different object types gracefully or check capabilities before invoking methods.
The
is_a?method provides runtime type checking, working seamlessly with inheritance hierarchies. This proves especially valuable when handling polymorphic associations in Rails:fido.is_a? Dog fido.is_a? PetBoth return
true, confirming that inheritance relationships are properly recognized throughout the class hierarchy.Let's verify our fluffy object's type as well:
fluffy.is_a? DogThis returns
falsesince fluffy is a direct instance of Pet, not Dog. Wait, that seems wrong based on our earlier code. Let me check this...Let's test with Sandra the reptile to see inheritance boundaries:
sandra.is_a? DogCorrectly returns
false. Sandra is a Reptile, not a Dog, even though both inherit from Pet. This demonstrates howis_a?respects class specificity while acknowledging inheritance relationships.The
respond_to?method offers another layer of introspection by checking whether an object can handle a specific method call. This enables defensive programming and graceful error handling.Create a Cat class with unique behavior to demonstrate method-specific introspection:
class Cat < Pet def meow puts "Meow!" endNow test whether objects respond to the Cat-specific
meowmethod. Ruby accepts both symbols and strings for method names, though symbols are preferred for performance reasons:fluffy = Cat.new('Miss Fluffy')fluffy.respond_to? :meowfluffy.respond_to? 'meow'
Both return
true, confirming that fluffy can respond to meow calls.Test method availability across different classes:
fido.respond_to? :meowReturns
false, as expected. Fido is a Dog and doesn't have access to Cat-specific methods. This type of checking proves invaluable in Rails applications where you're working with polymorphic associations or need to handle different object types gracefully without raising exceptions.
is_a? vs respond_to? Methods
| Feature | is_a? | respond_to? |
|---|---|---|
| Purpose | Checks class membership | Checks method availability |
| Works with inheritance | Yes - checks parent classes too | Yes - includes inherited methods |
| Example usage | fido.is_a? Pet | fluffy.respond_to? :meow |
| Return type | Boolean (true/false) | Boolean (true/false) |
Object Introspection Best Practices
Returns true for both immediate class and parent classes
Prevents NoMethodError exceptions in dynamic code
More idiomatic Ruby than passing strings
Ensures both correct type and method availability
While respond_to? accepts both symbols (:meow) and strings ('meow'), using symbols is more idiomatic in Ruby and slightly more efficient.