Friday, February 27, 2015

Callbacks in pure ruby - prepend over alias_method

I had a need to encrypt a password before an object was saved.  I searched around to find a gem that implemented callbacks in ruby but didn't find much.  I did come across this stack over flow post with some good recommendations.  Using the magical method_added worked pretty well for a single module included into a class.  However it didn't work so well when I had multiple modules inside my gem that were included in a class.  The original code looked something like this:

def method_added(method)
if !setting_callback
redefine_method(method) if is_a_callback?(method)
end
end
def redefine_method(original_method)
@setting_callback = true
original = instance_method(original_method)
define_method("#{original_method}_with_callbacks".to_sym) do |*args, &block|
trigger_callbacks(original_method, :before)
return_val = original.bind(self).call(*args, &block) if original
trigger_callbacks(original_method, :after)
return_val
end
alias_method original_method, "#{original_method}_with_callbacks".to_sym
@setting_callback = false
end
view raw gistfile1.rb hosted with ❤ by GitHub
This worked until I had a save method in my class and a save method in my module. The callback no longer worked.  The methods got stomped on when they were included.  So now onto the awesome prepend Module method.  This is a much cleaner way of prepending your module with an anonymous module that includes your method.  It is then properly in your call hierarchy so you can call it via super!

Here is what the callback code looks like now.  I feel like it is very readable and easy to comprehend:


module CallbackPureRuby
def self.included(klass)
klass.extend ClassMethods
klass.initialize_included_features
end
module ClassMethods
def initialize_included_features
@callbacks = Hash[:before, {}, :after, {}]
class << self
attr_accessor :callbacks
attr_accessor :setting_callback
end
end
def store_callbacks(type, method_name, *callback_methods)
callbacks[type][method_name] = callback_methods
if !setting_callback
prepend_method(method_name)
end
end
def method_missing(method, *args, &block)
if method.to_s =~ /^before|after$/
store_callbacks(method, *args)
else
super
end
end
def prepend_method(original_method)
@setting_callback = true
save_with_callbacks = Module.new do
define_method(original_method) do |*args, &block|
trigger_callbacks(original_method, :before)
return_val = super()
trigger_callbacks(original_method, :after)
return_val
end
end
prepend save_with_callbacks
@setting_callback = false
end
end
def trigger_callbacks(method_name, callback_type)
unless self.class.callbacks[callback_type][method_name].nil?
self.class.callbacks[callback_type][method_name].each{ |callback| __send__ callback }
end
end
end
view raw gistfile1.rb hosted with ❤ by GitHub
Feel free to post comments and feedback :)