In my previous post we saw how to add functionality to already existing objects thanks to the decorator pattern. Particularly, we discussed how to add more equipment (shield and armor) to different King objects which by default only had a sword. We also saw how to do it the Ruby way, mixing modules. But what if we want to add functionality not only to a single object, but to all objects from a class? There are three different ways to do this in Ruby: include
, prepend
and extend
, each of them has its own purpose. But before we can understand the differences between these three methods, we need to get an idea on how the Ancestors chain works.
The Ancestors chain
The Ancestors chain depends directly on the concept of the Ruby’s object model. For our needs today, we need to understand two key concepts of Ruby’s object model:
- Everything in Ruby is an object (even classes are objects).
- When you send a message to an object, but that object does not know how to handle it, it delegates the message to the next object in its Ancestors chain.
The implications of this are that the order in which a message is delegated is determined by the Ancestors chain because:
The Ancestors chain is the ordered collection of parent classes our object class inherits from plus all the modules that were mixed into it.
To understand this better, let’s create a class without any explicit parent class nor mixed module and see how its ancestor chain looks like:
1 2 3 |
irb> class MyClass; end irb> MyClass.ancestors => [MyClass, Object, PP::ObjectMixin, Kernel, BasicObject] |
In our example, whenever we send a message to MyClass
, it will try to handle the message itself, and if it is not capable of doing that, it will delegate the message to the next element in the Ancestors chain. In our example the Object
class, which will later do the same if it is not also able to handle the message, until BasicObject
, which is the root of Ruby Object’s model hierarchy, is reached.
The order in which the modules appear in our class’ Ancestors chain will be determined by the method we use to mix that module into our class. Which means, it will be different if we use include
, extend
or prepend
.
Let’s see how each of this alternatives affect the ancestor chain.
Include
When you use include within your class definition, it inserts the module in your class’ ancestor chain at the point in between your class and its parent class. To make an analogy, it would be as if your class’ new parent class was module you just included .
To see it more clearly, let’s see it with an example. Imagine we have our King class from our previous post, and we want all Kings to be able to greet. For that, we can include a Greeting
module with a greet
method to make the Kings able to greet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
module Greeting def greet puts 'Hello world!' end end class King include Greeting end King.ancestors # => [King, Greeting, Object, PP::ObjectMixin, Kernel, BasicObject] arthur = King.new arthur.greet # Hello world! |
As a side note, bear in mind that if you include several modules, the module which was included last will be the first to appear in the ancestors chain:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
module Greeting def greet puts 'Hello world!' end end module Sanctioning def sanction puts 'I hereby sanction this rule' end end class King include Greeting include Sanctioning end King.ancestors # => [King, Sanctioning, Greeting, Object, PP::ObjectMixin, Kernel, BasicObject] |
Prepend
Prepend
, on the other side, inserts the module before the class itself. That means, completely at the bottom of the ancestors chain. Like this:
That gives us the ability to add business logic around already existing methods of the class to which the module is prepended. Let’s see it within an example.
Do you remember the decorator example from the our post? Imagine that we want to decorate all King objects instead of individual ones. We can achieve that with prepend.
Note the use of super
as if we were referencing a parent class. We are in fact sending the same message to the next member of the ancestor chain that is able to handle it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
module ShieldDecorator def equipment super << 'shield' end end class King prepend ShieldDecorator def equipment %w[sword] end end King.ancestors # => [ShieldDecorator, King, Object, PP::ObjectMixin, Kernel, BasicObject] arthur = King.new arthur.equipment # =>=> ["sword", "shield"] |
Extend
Extend
is a little bit different to the previous methods we saw for including modules since it adds class methods, instead of adding instance methods.
In this example we extend our King class with a method to find Kings by name. For that, I have faked a King repository using a class variable. When a new King object is initialized, it gets added to this repo. To provide the King class with the find_by_name
finder method, we just need to extend the class King with the module implementing the finder.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
module KingFinder def find_by_name(name) kings.select { |king| king.name == name } end end class King extend KingFinder def self.kings @@kings ||= [] end def initialize(name) @name = name self.class.kings << self end attr_reader :name def inspect "King #{@name}" end end arthur = King.new('Arthur') aragorn = King.new('Aragorn') King.find_by_name('Arthur') # => [King Arthur] |
That was it for today! In an upcoming post I will explain you how you can both add instance and class methods with a single strike. Until then, enjoy coding!
AdiĆ³s!