Ruby’s Prepend: How is it useful?
With a real world example
This article introduced me to Prepend
. It does a really good job of explaining the keywords below & the ancestor hierarchy. As such it’s a good idea to read it before mine. I decided to write this to show my real world use-case of Prepend
, in hopes that it could help someone else understand better.
There are a few ways to modularize code in classes (or modules) in Ruby, namely:
Include
class Human
include Bipedalism
What this does is adds the methods in Bipedalism
to the Human
class as instance methods.
Extend
class Human
extend Bipedalism
What this does is adds the methods in Bipedalism
to the Human
class as class methods.
Prepend
To understand how Prepend
works, it’s important to know what the ancestors chain is.
If you’re familiar with inheritance (in any language), this should immediately make sense.
Functionality (methods) is shared in Ruby (and other languages) via inheritance.
When you call a method on an object, the Ruby interpreter tries to find that method definition in it’s class, if it doesn’t find it, it checks the class it inherits from (superclass), and so on till it does.
When you use Include
in a module, what the Ruby interpreter does is to inserts the included module right above the current class in the ancestor chain.
class Human
include Bipedalism
This means that Bipedalism
is the parent class of Human
.
However, what Prepend
does is slightly different. It puts the prepended module below the current class in the ancestor chain.
class Human
prepend Bipedalism
This means that Human
is the parent class of Bipedalism
.
At first, Prepend
seemed weird and I wondered what it could possibly be useful for, until I figured out this use case:
An Example
In BuyCoins’ back-end, we have a concept called a Bridge
. And we have many “Bridges
”.
For a class to be a Bridge
(in our codebase), it needs to respond to certain method calls. Let’s just imagine that there’s only 2 of them; get_address
& balance
.
Every class that is meant to be a Bridge
will need to implement those methods (because duck typing).
At this point, everything is relatively simple. I could create FirstBridge
& SecondBridge
as follows:
class FirstBridge
def get_address
# do whatever
end
def balance
# do some magic
end
end
class SecondBridge
def get_address
# do whatever
end
def balance
# do some magic
end
end
I however had this requirement that made things a bit difficult. I wanted it such that a validation method (validate_supported_currency
) is first called every time any method (get_address
, balance
) in any Bridge
is called.
One way to do this would be to copy and paste this validation method in every Bridge
class. And then manually invoke it in all the methods.
class SecondBridge
def get_address
validate_supported_currency
# do the things to get address
end
def balance
validate_supported_currency
# do the magic
end
def validate_supported_currency
raise Exceptions::UnsupportedCurrencyError if conditions_not_fulfilled
endend
This is a stressful solution, because it requires me to repeatedly define validate_supported_currency
and then remember to make sure it’s called in every method.
One other option would be to define a Bridge
module that defines validate_supported_currency
and then I could include
that in every class that’s meant to be a Bridge.
module Bridge
def validate_supported_currency
raise Exceptions::UnsupportedCurrencyError if conditions_not_fulfilled
end
end
class FirstBridge
include Bridge
def get_address
validate_supported_currency
# do the things to get address
end
def balance
validate_supported_currency
# do the magic
end
end
This solves the problem of having to define validate_supported_currency
repeatedly but I still have to remember to invoke it in every single method of every Bridge
class that I create.
To solve that, I decided to use prepend
. This means that I first needed to make some additions to the Bridge
module.
module Bridge
def get_address
validate_supported_currency
super
end
def balance
validate_supported_currency
super
end
def validate_supported_currency
raise Exceptions::UnsupportedCurrencyError if conditions_not_fulfilled
end
end
super
simply calls the method of the same name in the parent class of the current class.
Let’s look at get_address
, what this means is that after calling validate_supported_currency
Ruby will try to see if our current object has a parent class/module with get_address
defined and then invoke it if it does. You can read more about super
and overriding methods here.
So how do we get this to work in our actual Bridge classes? Simple:
class AnotherBridge
prepend Bridge
def get_address
# do the tings fam
end
def balance
# magic time
end
end
And that’s it! By simply prepend
ing Bridge
, every time get_address
or balance
is called, validate_supported_currency
will be called first before running the code that’s actually defined in the methods themselves.
This is because (as explained above) prepend
makes AnotherBridge
the parent class of Bridge
.
As such, when get_address
is called, the Ruby interpreter will invoke the get_address
as is defined in Bridge
first. And this invokes validated_supported_currency
.
Then we call super
in the next line which then invokes the get_address
defined in AnotherBridge
(the parent class).
That’s all for today! Go forth and prepend
!