Method Triggers: Instead, Before, After
Join the DZone community and get the full member experience.
Join For FreeRedefine method calls, in manner similar to SQL triggers and PostgreSQL rules - i.e. execute code block instead, before or after call to original method. rules can be appended and removed.
class Object
def self.__rules__
# container for defined rules, each item is:
# [class, event_name, method_name, alias_for_original_method, caller, comment]
@@rules ||= []
end
def self.__create_rule_instead( method, comment = '', &block) # creates and returns new rule
b_id = "%04x" % block.object_id
old_method_name = :"__previous_#{method}_#{b_id}"
alias_method old_method_name, method
define_method method, &block
__rules__ << rule = [self, "INSTEAD", method, old_method_name,caller[0], comment ]
rule
end
def self.__create_rule_before( method, comment = '', &block)
args = instance_method(method).arity == 0 ? '' : '(*args)'
b_id = "%04x" % block.object_id
old_method_name = :"__previous_#{method}_#{b_id}"
alias_method old_method_name, method
define_method :"__before_#{method}_#{b_id}", &block
class_eval <<-EOT
def #{method}#{args}
__before_#{method}_#{b_id}#{args}
__previous_#{method}_#{b_id}#{args}
end
EOT
__rules__ << rule = [self, "BEFORE", method, old_method_name, caller[0], comment]
rule
end
def self.__create_rule_after( method, comment = '', &block)
args = instance_method(method).arity == 0 ? '' : '(*args)'
b_id = "%04x" % block.object_id
old_method_name = :"__previous_#{method}_#{b_id}"
alias_method old_method_name, method
define_method :"__after_#{method}_#{b_id}", &block
class_eval <<-EOT
def #{method}#{args}
res = __previous_#{method}_#{b_id}#{args}
__after_#{method}_#{b_id}#{args}
res
end
EOT
__rules__ << rule = [self, "AFTER", method, old_method_name,caller[0], comment ]
rule
end
def self.__remove_rule( rule ) # has some bugs when rules on subclasses are defined :(
idx = __rules__.index(rule)
if idx
# look for next rule for the same method
idx += 1
while idx < __rules__.size
break if __rules__[idx][2] == rule[2] && __rules__[idx][0] == rule[0]
idx+=1
end
if idx < __rules__.size
next_rule = __rules__[idx]
next_rule[0].send :remove_method, next_rule[3]
next_rule[0].send :alias_method, next_rule[3], rule[3]
else
# that was last
rule[0].send :remove_method, rule[2]
rule[0].send :alias_method, rule[2], rule[3]
end
__rules__.delete(rule)
end
end
end
Example:
class Model
def save
puts "save"
end
def reload(flag)
"reloaded"
end
end
r1 = Model.__create_rule_instead(:reload) {|flag| flag ? "FRESH" : "STALE" }
obj = Model.new
puts "RELOAD:"+obj.reload(true)
puts "RELOAD:"+obj.reload(false)
Object.__remove_rule(r1)
puts "RELOAD:"+obj.reload(false)
Model.__create_rule_before(:save) { puts "BEFORE SAVE" }
r2 = Model.__create_rule_before(:save) { puts "YET BEFORE SAVE" }
Model.__create_rule_after(:save) { puts "AFTER SAVE" }
r3 = Model.__create_rule_after(:save) { puts "YET AFTER SAVE" }
obj.save
Object.__remove_rule(r2)
puts "----------"
obj.save
Object.__remove_rule(r3)
puts "----------"
obj.save
produces:
RELOAD:FRESH
RELOAD:STALE
RELOAD:reloaded
YET BEFORE SAVE
BEFORE SAVE
save
AFTER SAVE
YET AFTER SAVE
----------
BEFORE SAVE
save
AFTER SAVE
YET AFTER SAVE
----------
BEFORE SAVE
save
AFTER SAVE
Opinions expressed by DZone contributors are their own.
Comments