Prepend in Ruby
There are three ways to include modules in Ruby: include, extend and prepend. While first two are commonly used everywhere, but we don’t see prepend used often. Actually, I can’t remember any case in my career where I’ve seen that method. I’ve been interviewing many people recently and even on senior positions people don’t seem to know what it actually does. So, in this article I am going to explain its functionality and give as many more or less real world use cases as I can.
First of all, let me explain what it does. All methods above include module to inheritance chain of a Ruby class. Include and extend add method declared in the module up in inheritance chain and normally used as mixins to reuse code. Difference between those two is whether to add module method to class or to instance.
module UseMe
def foo
p 'hello'
end
end
class MyClassOne
include UseMe
end
# MyClassOne.new.foo
# => hello
class MyClassTwo
extend UseMe
end
# MyClassTwo.foo
# => hello
Inheritance chain goes like this:
p MyClassOne.ancestors # same for MyClassTwo
# => [MyClassOne, UseMe, Object, Kernel, BasicObject]
Prepend adds module method down into inheritance chain, which means that method from module will actually executed instead of method, defined in class.
class MyClassThree
prepend UseMe
end
MyClassThree.new.foo
# => hello
p MyClassThree.ancestors
# => [UseMe, MyClassThree, Object, Kernel, BasicObject]
In the example above we used prepend the same way as the other two, and seemingly there is no difference yet. But the fun starts when we add method into class with the same name.
module UseMe
def foo
p 'hello'
end
end
class MyClassThree
prepend UseMe
def foo
p 'I think I am being ignored here'
end
end
MyClassThree.new.foo
# => hello
Because UseMe is now lower in inheritance chain it will be executed instead of class method, which will be completely ignored until we add super() to module method.
module UseMe
def foo
p 'hello'
super() # using of brackets is because of Ruby style guide
end
end
class MyClassThree
prepend UseMe
def foo
p 'Now I am executed'
end
end
MyClassThree.new.foo
# => hello
# => Now I am executed
Real world prepend use cases
Disclaimer: use of prepend sometimes can cause too much implicit behavior in code, be careful to not confuse other developers.
Use case #1: Logging
module Loggable
def create_user(*args)
Rails.logger.debug "Creating user with params #{args}"
super(*args)
Rails.logger.debug "User created"
end
end
class UserService
prepend Loggable
def create_user(email, name)
User.create(email: email, name: name)
end
end
Here we have rails-ish app as an example and if we want to log certain methods without touching actual methods, we can prepend Loggable module and magic will do work.
Use case #2: Transaction
module Transactionable
def create_user(*args)
ActiveRecord::Base.transaction do
super(*args)
rescue
Rails.logger.error "Transaction for creating users failed"
end
end
end
We can also wrap methods in transactions and maybe add some transaction related logic in prepended module.
Use case #3: Debugging
module Debuggable
def do_something(*args)
time_start = Time.now
super(*args)
time_end = Time.now
p "Time spent: #{time_end - time_start}"
end
end
Regarding cases above, there might be a good question: do we actually need to declare each method class uses in modules like Loggable? That would basically mean that we won’t be able to reuse module for anything that doesn’t contain same methods as the class it was included in.
I have weird looking but working solution for that as well.
module Loggable
def initialize
self.class.ancestors[1].instance_methods(false).each do |method_name|
Loggable.define_method(method_name) do
super()
p "Method #{method_name} was invoked"
end
end
end
end
class UserService
prepend Loggable
def create_user
p 'creating user'
end
end
With some touch on meta programming, now we have prepended versions of methods for any method define in class.
So, while prepend is much less intuitive than its siblings and in some cases can lead into implicit ‘too magical’ approach, it’s pretty solid tool which can perfectly fit into some cases, so let’s not forget about this nice Ruby feature.