If youve 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 didnt seem to use the code in the method definition youre 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!
๐ง๐พ๐๐
Top comments (0)