Doing Aspect-Oriented Programming
Problem
You want to "wrap" a method with new code, so that calling the method triggers some new feature in addition to the original code.
Solution
You can arrange for code to be called before and after a method invocation by using method aliasing and metaprogramming, but it's simpler to use the glue gem or the AspectR third-party library. The latter lets you define "aspect" classes whose methods are called before and after other methods.
Here's a simple example that traces calls to specific methods as they're made:
require 'aspectr' class Verbose < AspectR::Aspect def describe(method_sym, object, *args) "#{object.inspect}.#{method_sym}(#{args.join(",")})" end def before(method_sym, object, return_value, *args) puts "About to call #{describe(method_sym, object, *args)}." end def after(method_sym, object, return_value, *args) puts "#{describe(method_sym, object, *args)} has returned " + return_value.inspect + '.' end end
Here, I'll wrap the push and pop methods of an array. Every time I call those methods, the aspect code will run and some diagnostics will be printed.
verbose = Verbose.new stack = [] verbose.wrap(stack, :before, :after, :push, :pop) stack.push(10) # About to call [].push(10). # [10].push(10) has returned [[10]]. stack.push(4) # About to call [10].push(4). # [10, 4].push(4) has returned [[10, 4]]. stack.pop # About to call [10, 4].pop(). # [10].pop() has returned [4].
Discussion
There's a pattern that shows up again and again in Ruby (we cover it in Recipe 7.10). You write a method that performs some task-specific setup (like initializing a timer), runs a code block, then performs task-specific cleanup (like stopping the timer and printing out timing results). By passing in a code block to one of these methods you give it a new aspect: the same code runs as if you'd just called Proc#call on the code block, but now it's got something extra: the code gets timed, or logged, or won't run without authentication, or it automatically performs some locking.
Aspect-oriented programming lets you permanently add these aspects to previously defined methods, without having to change any of the code that calls them. It's a good way to modularize your code, and to modify existing code without having to do a lot of metaprogramming yourself. Though less mature, the AspectR library has the same basic features of Java's AspectJ.
The Aspect#wrap method modifies the methods of some other object or class. In the example above, the push and pop methods of the stack are modified: you could also modify the Array#push and Array#pop methods themselves, by passing in Array instead of stack.
Aspect#wrap aliases the old implementations to new names, and defines the method anew to include calls to a "pre" method (@Verbose#before in the example) and/or a "post" method (@Verbose#after in the example).
You can wrap the same method with different aspects at the same time:
class EvenMoreVerbose < AspectR::Aspect def useless(method_sym, object, return_value, *args) puts "More useless verbosity." end end more_verbose = EvenMoreVerbose.new more_verbose.wrap(stack, :useless, nil, :push) stack.push(60) # About to call [10].push(60). # More useless verbosity. # [10, 60].push(60) has returned [[10, 60]].
You can also undo the effects of a wrap call with Aspect#unwrap.
verbose.unwrap(stack, :before, :after, :push, :pop) more_verbose.unwrap(stack, :useless, nil, :push) stack.push(100) # => [10, 60, 100]
Because they use aliasing under the covers, you can't use AspectR or glue to attach aspects to operator methods like <<. If you do, AspectR (for instance) will try to define a method called __aop__singleton_<<, which isn't a valid method name. You'll need to do the alias yourself, using a method name like "old_lshift", and define a new << method that makes the pre- and post-calls.
See Also
- The AspectR home page is at http://aspectr.sourceforge.net/
- Recipe 7.10, "Hiding Setup and Cleanup in a Block Method"
- Recipe 10.14, "Aliasing Methods"
- Recipe 20.4, "Synchronizing Access to an Object"