Ruby Method Lookup Demystified: Inheritance, Mixins, and Super
Discover how method lookup works in Ruby, including inheritance, mixins using include, prepend, and extend, and the super method.
If you’ve worked on Ruby projects for a while, you would have encountered times when you were digging to find out how and why a certain method behaved the way it did, because it didn’t seem to use the code in the method definition you’re looking at. And what happens when there are multiple methods with the same name?
This can seem confusing, but Ruby does have a specific way or path that is used to determine what methods to actually call. This process is called “method lookup”. In this guide we will delve into the details of method lookup, covering inheritance, mixins using include
, prepend
, and extend
, as well as the super
method. With clear examples and explanations, this article will help you build a strong foundation and understanding that enables you to write more maintainable code.
Inheritance and Method Lookup in Ruby
In Ruby, inheritance allows a class to inherit the methods and attributes of another class. The class that inherits is called a subclass, and the class that is inherited from is called a superclass. Method lookup in Ruby begins by searching the instance methods of the object's class, and if not found, it moves up the class hierarchy until it reaches the superclass. Let's consider an example:
class Animal
def speak
"I'm an animal!"
end
end
class Dog < Animal
end
dog = Dog.new
puts dog.speak # Output: I'm an animal!
In the example above, the Dog
class inherits from the Animal
class. When we call the speak
method on a Dog
object, Ruby first looks for the method in the Dog
class. Since it doesn't find it there, it searches the superclass Animal
, finds the speak
method, and executes it.
Mixins and Method Lookup
Ruby uses modules as a way to implement multiple inheritance through mixins. Mixins are used to share methods among different classes, making your code more modular and easier to maintain. There are three primary ways to include a module in your class: include
, prepend
, and extend
.
Include
include
adds the module's methods as instance methods in the class. When a method is called on an object, Ruby first searches the object's class, then the included module, and finally the superclass.
module Speak
def speak
"I'm a speaking mixin!"
end
end
class Dog < Animal
include Speak
end
dog = Dog.new
puts dog.speak # Output: I'm a speaking mixin!
In this example, we've included the Speak
module in the Dog
class. When we call the speak
method on a Dog
object, Ruby first looks for the method in the Dog
class, then in the included Speak
module, and finally in the superclass Animal
. Since it finds the speak
method in the Speak
module, it executes that method.
Prepend
prepend
is similar to include
, but it inserts the module's methods before the class in the method lookup chain.
class Dog2 < Animal
prepend Speak
end
dog = Dog2.new
puts dog.speak # Output: I'm a speaking mixin!
In this case, when we call the speak
method on a Dog
object, Ruby first looks in the prepended Speak
module, then in the Dog
class, and finally in the superclass Animal
. Since it finds the speak
method in the Speak
module, it executes that method.
Extend
extend
adds the module's methods as class methods, rather than instance methods. This means that the methods can be called directly on the class itself.
class Dog < Animal
extend Speak
end
puts Dog.speak # Output: I'm a speaking mixin
In this example, by using extend
instead of include
or prepend
, we've added the speak
method as a class method of Dog
. This allows us to call the speak
method directly on the Dog
class.
The super
Method
The super
method in Ruby allows a subclass to call a method from its superclass. This can be useful when you want to extend the behaviour of a superclass method in a subclass without completely overriding it. Let's see an example:
class Animal
def speak
"I'm an animal!"
end
end
class Dog < Animal
def speak
"#{super} and I'm a dog!"
end
end
dog = Dog.new
puts dog.speak # Output: I'm an animal! and I'm a dog!
In the Dog
class, we've defined a speak
method that calls the speak
method from its superclass Animal
using the super
keyword. This allows us to extend the behaviour of the Animal#speak
method in the Dog
class.
The super
Method with Mixins
The super
methods can still be used when mixins are involved, and for the purposes of this post is a great showcase of the method lookup flow.
In this example, we'll demonstrate the use of super
in an object that uses both prepend
and include
to mixin the same method. We'll have three modules with a greet
method, and a Person
class that will include and prepend those modules.
module GreetInEnglish
def greet
"Hello! #{super}"
end
end
module GreetInSpanish
def greet
"¡Hola! #{super}"
end
end
module GreetInFrench
def greet
"Bonjour! #{super}"
end
end
class LivingBeing
prepend GreetInEnglish
def greet
"I'm a living being."
end
end
class Person < LivingBeing
include GreetInFrench
prepend GreetInSpanish
def greet
"I'm a person. #{super}"
end
end
person = Person.new
puts person.greet
In this example, we've added a LivingBeing
class that includes GreetInEnglish
and has its own greet
method. The Person
class now inherits from LivingBeing
, includes GreetInFrench
, and prepends GreetInSpanish
.
When we call person.greet
, the method lookup order is as follows:
GreetInSpanish#greet
(prepended inPerson
)Person#greet
(defined in the class)GreetInFrench#greet
(included inPerson
)GreetInEnglish#greet
(prepended inLivingBeing
)LivingBeing#greet
(inherited from the superclass)
The output will be:
¡Hola! I'm a person. Bonjour! Hello! I'm a living being.
Now, the method lookup chain correctly includes all mixins and the inherited method from LivingBeing
. The super
keyword is used in each of the mixin methods and the Person#greet
method to call the next method in the lookup chain.
Conclusion
Understanding Ruby's method lookup process helps you write more maintainable code and tackle complex Ruby projects and libraries. Use inheritance, mixins, and the super
method to override and extend capabilities in Ruby when needed. Be mindful of code complexity and avoid hiding implementation details in too many places. Happy coding!
🧑🏾🚀🚀