Previously, we learned about the differences between class inheritance and modules in Ruby. You can read more about that here. In that post, we reviewed how to use modules to share functionality between classes that do not have a clear hierarchical arrangement. Specifically, we used the include
keyword to give our classes access to instance methods. In this post, we will focus on digging deeper into modules in Ruby by reviewing the use of the include
keyword and introducing two other keywords, extend
and prepend
. Understanding the different behaviors of each of these three keywords and knowing when to use them will provide us with the power to utilize modules to extend functionality to our classes in a variety of ways.
include
include
is the keyword that we will use within our classes to provide access to the methods defined in a module as instance methods in our class. In order to do so, we will first define a module.
module IndoorPet
def can_be_housebroken?
true
end
end
Now, all we have to do to give our classes access to the instance method can_be_housebroken?
as it is defined in our IndoorPet module is add include IndoorPet
in the body of our class when we define it.
class Animal
end
class Dog < Animal
include IndoorPet
end
class Cat < Animal
include IndoorPet
end
class Rabbit < Animal
include IndoorPet
end
roger = Rabbit.new("Roger")
roger.can_be_housebroken?
=> true
Rabbit.can_be_housebroken?
=> NoMethodError: undefined method `can_be_housebroken?' for Rabbit:Class
With that, we have now included the module's instance method can_be_housebroken?
in our Dog, Cat, and Rabbit classes. Thus, any instances of those classes will now have access to the can_be_housebroken?
method.
When you include
a module, Ruby will insert the module into the class's ancestry chain just above our class, between our class and it's superclass. This chain can be seen when calling .ancestors
on our Rabbit class.
Rabbit.ancestors
=> [Rabbit, IndoorPet, Animal, Object, PP::ObjectMixin, Kernel, BasicObject]
extend
Now that we know how to use include to provide a class with access to a module's instance methods, you might be wondering how to provide a class with access to a module's class methods. The extend
keyword does just that. It is used in exactly the same way as include, except that instead of instance methods which can be called on individual instances of our class, all of the methods we are extending to our class will be provided as class methods we can call on the class as a whole.
Instead of including can_be_housebroken?
as instance method, let's try extending it as a class method. First, we define our module.
module IndoorPet
def can_be_housebroken?
true
end
end
Then we use the extend
keyword the same way we used the include
keyword previously.
class Animal
end
class Dog < Animal
extend IndoorPet
end
class Cat < Animal
extend IndoorPet
end
class Rabbit < Animal
extend IndoorPet
end
roger = Rabbit.new("Roger")
roger.can_be_housebroken?
=> NoMethodError: undefined method `can_be_housebroken?' for #<Rabbit:0x00007fa84389c748>
Rabbit.can_be_housebroken?
=> true
With that, we have now extended the module's class method can_be_housebroken?
to our Dog, Cat, and Rabbit classes. Now those classes as a whole have access to the can_be_housebroken?
method. Note that you can no longer call can_be_housebroken?
on an individual instance of the class. With extend
, we have only given the method to the class as a class method.
When you extend a module, ruby will provide the module's methods to the class as class methods. However, unlike with include
, when you extend
a module, Ruby will not insert the module into the class's ancestry chain. We can see this by calling .ancestors
on our Rabbit class.
Rabbit.ancestors
=> [Rabbit, Animal, Object, PP::ObjectMixin, Kernel, BasicObject]
prepend
The third keyword for providing a module's methods to a class is prepend
. We can use prepend
in the same way we would use include
or extend
by first defining the module and then using the keyword and name of the module within the class's definition, as below.
module IndoorPet
def can_be_housebroken?
true
end
end
class Animal
end
class Dog < Animal
prepend IndoorPet
end
class Cat < Animal
prepend IndoorPet
end
class Rabbit < Animal
prepend IndoorPet
end
In many ways, prepend
operates like include
. Both provide a class access to a module's methods as instance methods. Therefore, we can call the imported methods that we have prepended from our module on specific instances of a class, and not on the class itself.
roger = Rabbit.new("Roger")
roger.can_be_housebroken?
=> true
Rabbit.can_be_housebroken?
NoMethodError: undefined method `can_be_housebroken?' for Rabbit:Class
However, the difference between include
and prepend
is related to the location in the ancestry chain where the module is placed. With prepend
, the module is not inserted between the class and it's superclass as it was with include
. It is actually inserted at the very bottom of the ancestry chain, as seen below.
Rabbit.ancestors
=> [IndoorPet, Rabbit, Animal, Object, PP::ObjectMixin, Kernel, BasicObject]
The importance of this distinction will be explored further in our next post!
include
vs extend
vs prepend
include
- Provides a class with access to a module's methods as instance methods
- Allows the methods to be called on individual instances of the class
- Does not allow methods to be called on the class as a whole
- Inserts module into ancestry chain between the class and superclass
extend
- Provides a class with access to a module's methods as class methods
- Does not allow the methods to be called on individual instances of the class
- Allows methods to be called on the class as a whole
prepend
- Provides a class with access to a module's methods as instance methods
- Allows the methods to be called on individual instances of the class
- Does not allow methods to be called on the class as a whole
- Inserts module at the bottom of the ancestry chain
Top comments (4)
Early in this article it is stated:
But those are not keywords, but methods:
I want to send some new programmers here to read up on this, but also want them to have the right names for things.
Good articles, but I see one small bug (for learners): roger object can't be initialized with argument in this case, because Rabbit class doesn't allow it (Rabbit or Animal classes dont't have initalize method). Good luck :-)
Easy to follow and crisp explanation! Thanks!
thanks for the simple explanation!