Module#prepend
is a feature added in Ruby 2.0. Until now i didn’t have an oportunity to try it. Turns out it’s pretty cool and useful!
When you call a method, Ruby looks on the receiver’s ancestor chain from left to right and invokes method from the first member which implements it.
If you call super
keyword inside a method Ruby invokes same method on next member which implements it. Unfortunately, there is no way how to call same method on previous member.
Now this is how ancestor chain might look in Ruby 2.0:
[object’s singleton class, prepended modules, object’s class itself, included modules, superclasses]
When you prepend
a module it is actually inserted before the class itself in ancestor chain, not after as it’s with include
. Thus when you define same method in the prepended module and the class and then you call it the method from prepended module is actually invoked.
Demonstration:
Before Ruby 2.0 this wouldn’t be possible.
I have few service classes which all have same structure. These classes serve as simple data modifiers. It’s something like this:
module Contact
class PostalCodeNormalizer
attr_accessor :raw, :modified, :invalid
def intialize(raw_contacts)
@raw = raw_contacts
@modified = []
@errors = []
end
def call
raw.each do |contact_info|
if contact_info[:postal_code].blank?
errors << [contact_info, 'Postal code is missing!']
else
contact_info[:postal_code].delete(' ')
modified << contact_info
end
end
if raw.size == (modified.size + errors.size)
[modified, errors]
else
fail "sums aren't same"
end
end
end
end
I have a bunch of these with different functions. I chain them as i need, collect the modified data and errors and do something with them. But the size checking code at the end of #call
method is always the same.
With Module#prepend
it can be very nicely refactored:
module Contact
module BaseModifier
...
def call
super
if raw.size == (modified.size + errors.size)
[modified, errors]
else
fail "sums aren't same"
end
end
end
class PostalCodeNormalizer
prepend BaseModifier
def call
raw.each do |contact_info|
if contact_info[:postal_code].blank?
errors << [contact_info, 'Postal code is missing!']
else
contact_info[:postal_code].delete(' ')
modified << contact_info
end
end
end
end
end
Before Module#prepend
i would have to do something like this:
module Contact
module BaseModifier
...
def call
do_call
...
end
def do_call
fail 'implement in subclass'
end
end
class PostalCodeNormalizer
include BaseModifier
def do_call
...
end
end
end
Or something like this: Module.prepend: a super story
Have a good one!