Quantcast
Channel: The Sleepless Geek
Viewing all articles
Browse latest Browse all 25

Metaprogramming Danger Box

$
0
0
In my last post, I showed how one can use the "execute around" pattern in Ruby to log any calls to a method, without cluttering the methods themselves with logging calls.

That gave us code like this:

def defuse_bomb
with_log('defuse_bomb') {puts "Snip the red wire..."}
end

Our with_log method took care of printing out some logging info before and after executing the block given to it.

But as Adam pointed out in the comments, this is still kind of ugly. We don't have all the actual logging code inside of the "defuse_bomb" method, but have had to modify it to use with_log. What if we could add logging more transparently?

We can! With some metaprogramming magic added to the Logging module, here's the way the DangerBox class looks now:

class DangerBox
extend Logging

add_logging_to(:defuse_bomb, :throw_error)

def defuse_bomb
puts "Snip the red wire..."
end

def throw_error
puts 'I feel an error coming on...'
raise 'Success: an error!'
end

# This method's output won't be logged
def eat_poison
puts "Mmmmm, poison!! Nom Nom Nom Nom!!!!"
end

end

It still works the same way as it did before:

d = DangerBox.new
d.defuse_bomb
# ==== Sun Jun 12 16:55:49 -0400 2011 ====
# About to execute 'defuse_bomb'...
# Snip the red wire...
# Executed 'defuse_bomb' successfully.

d.throw_error
# ==== Sun Jun 12 16:55:49 -0400 2011 ====
# About to execute 'throw_error'...
# I feel an error coming on...
# Error while executing 'throw_error': 'Success: an error!'!

d.eat_poison
# Mmmmm, poison!! Nom Nom Nom Nom!!!!

Adam, I see your "hmmmm" and raise you a "BOOYAH!" (Although I didn't write tests for my version of this, so you still win on that front.)

But how the heck did we get all this logging output? Obviously, it's something to do with the line that says:

add_logging_to(:defuse_bomb, :throw_error)

Well, yes: that and a spiffy new Logging module that redefines our methods on the fly to use logging. Here's the code:

module Logging
# Keeps track of the methods we want to log
def add_logging_to(*args)
@logged_methods ||= []
@logged_methods.concat(args)
end

# Anytime a method gets added to our class (like with "def"),
# this will execute
def method_added(method_name)
if @logged_methods.include?(method_name)
# A lock to prevent infinite recursion; since we're
# going to add a method below, we don't want that
# to make method_added fire again, which would
# make it fire again, etc...
unless @adding_with_metaprogramming
@adding_with_metaprogramming = true
stashed_method = "stashed_#{method_name}".to_sym
# Creates a new method named, for example,
# "stashed_defuse_bomb", which does the same
# thing as "defuse_bomb"
alias_method stashed_method, method_name
# Now we redefine "defuse_bomb" to do this:
define_method method_name do
begin
puts "==== #{Time.now} ===="
puts "About to execute '#{method_name}'..."
# Calls "stashed_defuse_bomb" to get the original version
send(stashed_method)
puts "Executed '#{method_name}' successfully."
rescue Exception => e
puts "Error while executing '#{method_name}': '#{e.message}'!"
ensure
puts "\n\n"
end
end
end
# Remove the lock
@adding_with_metaprogramming = false
end
end
end

Metaprogramming! It sounds terrifying, doesn't it? Like, if programs can write programs, next thing you know, the Terminator is going to be ripping through your door! But it's actually slightly less terrifying than that.

Anyway, I got the idea for the last post from reading one chapter in "Eloquent Ruby," and I got the idea for this post while lying in bed after reading another chapter. So: thumbs up, Eloquent Ruby! I hereby recommend you.

Viewing all articles
Browse latest Browse all 25

Latest Images

Trending Articles



Latest Images