Metaprogramming.apply(Aspects)

Posted by Mathew Abonyi Wed, 10 Oct 2007 01:51:00 GMT

I’m just briefly breaking my silence (which must continue a bit longer) to raise awareness about an impressive and very useful gem which has just come onto the scene: Aquarium.

Although I don’t follow Aspect Oriented Programming very closely, I know about its benefits and use it when necessary. All Rails developers, for example, employ its principles without even knowing it: before, after and around filters, proxies to alter execution, callbacks and the like. If you don’t know about AOP yet, basically it’s the practice of separating components which appear across the various classes of an application to provide system or class-agnostic functionality—for example, logging, authentication or object persistence.

Having these components intrude on, for example, calculating a bank balance really gets annoying after a while. Imagine every moderately sized script you write needing lots of diagnostic messages, custom event handlers, and the like. It gets very tedious because you know there’s the ideal of reuse just around the corner.

Clean, Ruby-esque AOP… a Pipe Dream?

Imagine methods without calls to a logger, without a litter of ‘if/then/else’ for authentication or authorisation, without calls to save an object and checking if it returns true or false… What you’ll get is the raw functionality of your class or module, which… umm, is as it should be. Wrapping a method (using cuts, wrapping or aspects from AOP) lets you extract common functionality which cuts across objects.

Until now, there’s only been the Facets libraries for AOP (‘advice’, ‘aop’ and ‘cut’ in Facets 2.0.0) and AspectR. The former is buggy at best and the latter is like returning to Java. So I wasn’t very keen on AOP in Ruby at all. Metaprogramming seemed easy enough and didn’t need the structure of AOP to follow.

Enter Aquarium. It’s a glass of clean water in a desert. Ruby-style. The most important thing it offers is a ‘Join Point Model’ which will make you wet. In other words, it is very easy to intercept method calls to wrap them and introduce functionality before or after the original definition.

Sadly, I don’t have the time for an extensive example from my own coding, so I’ll have to rip off the example from Aquarium’s own README which I think illustrates its power best (with a few modifications to show how I use it at the moment): method tracing.

Example: Method Tracing


require 'rubygems'
require 'aquarium'
require 'aquarium/aspects/dsl/object_dsl' # makes the Aspects DSL (before/after/around) system-wide

module Example
  class Foo
    def initialize *args
      p "Inside:   Foo#initialize: args = #{args.inspect}"
    end
    def do_it *args
      p "Inside:   Foo#do_it: args = #{args.inspect}"
    end
  end

  module BarModule
    def initialize *args
      p "Inside:   BarModule#initialize: args = #{args.inspect}"
    end
    def do_something_else *args
      p "Inside:   BarModule#do_something_else: args = #{args.inspect}"
    end
  end

  class Bar
    include BarModule
  end
end

p "Before advising the methods:"
foo1 = Example::Foo.new :a1, :a2
foo1.do_it :b1, :b2

bar1 = Example::Bar.new :a3, :a4
bar1.do_something_else :b3, :b4

# EXAMPLE 1: Wrapping methods on multiple classes using the powerful 'types' matcher.
# It can take constants or regular expressions. Also demonstrates method_options, which
# can specify public, protected, private, inherited or local methods.

module Example
  include Aquarium::Aspects::DSL::AspectDSL
  around :types => [Foo, Bar], :methods => :all, 
  :method_options => :suppress_ancestor_methods do |jp, *args|
    begin
      p "Ex. 1: Entering: #{jp.target_type.name}##{jp.method_name}: args = #{args.inspect}"
      jp.proceed
    ensure
      p "Ex. 1: Leaving:  #{jp.target_type.name}##{jp.method_name}: args = #{args.inspect}"
    end
  end
end

# EXAMPLE 2: Wrapping one or more methods very simply.
module Example
  class Bar
    after :do_something_else do |jp, *args|
      p "Ex. 2: Simply after: #{jp.target_type.name}##{jp.method_name}: args = #{args.inspect}"
    end
  end
end

# EXAMPLE 3: Using the Aspect class without the DSL

include Aquarium::Aspects
Aspect.new :before, :method => :do_it, :type => Example::Foo do |jp, *args|
  p "Ex. 3: Simply before (using Aspect.new): #{jp.target_type.name}##{jp.method_name}: args = #{args.inspect}"
end

# EXAMPLE 4: Contain aspects within a namespace for clarity. These are executed
# upon loading the source.

module Example
  module AllAspects
    include Aquarium::Aspects::DSL::AspectDSL
    before :do_it, :type => Example::Foo do |jp, *args|
      p "Ex. 4: Simply before (using namespaces): #{jp.target_type.name}##{jp.method_name}: args = #{args.inspect}"
    end
  end
end

# And now see the result...
puts
p "After advising the methods. Notice that #intialize isn't advised:"
foo2 = Example::Foo.new :a5, :a6
foo2.do_it :b5, :b6

bar1 = Example::Bar.new :a7, :a8
bar1.do_something_else :b7, :b8

You could argue that the namespace for Aquarium needs a little work and the availability of Aspect.new system wide should be a given, but adding that oneself until Aquarium has it is no big deal. I also didn’t demonstrate pointcuts, another option to around, before and after. With a pointcut, you can encapsulate the type, scope and method names—perhaps as a constant, class variable or class method to reduce dependencies.

Overview

All in all, the DSL is already miles better than anything else out there and Aquarium was only released to the public months ago. To summarise, I think this DSL is killer because it is…

  • clear about what is affected
  • easy to separate from what is affected
  • rich with selectors (type, scope, name, pointcut)
  • able to choose whether to alter method execution

So, I would definitely suggest giving it a go and seeing how it can modularise some of your ‘cross cutting concerns’. I also suggest the Aquarium README and the reading recommended on its site, http://aquarium.rubyforge.org/.

Posted in ,  | Tags , , , , ,  | 2 comments | 1 trackback

Comments

  1. simplicoder said 6 months later:

    Was this silence aforementioned?

    Oh well.

    I’ve always enjoyed your blogs… and creations.

    Will continue to keep you in the feed reader.

  2. Mathew Abonyi said 6 months later:

    I am still around and watching the blog - or at least making sure it is alive and up - but there just hasn’t been the time to post anything.

    There have been a lot of utilities which I’ve noticed and would like to post about; a lot of techniques I’ve learned over the last year; a lot of changes to my own code that I’d like to share. It’s just about finding the time.

    Thanks for the support—I’ll try to be more vocal in the near future when I’ve freed up some time. I think realistically it’ll be about a month from now before I’m properly back, though.

    Things I’m considering posting about:
    • using EventMachine
    • using scraping toolkits
    • parsing & executing JavaScript in Ruby
    • writing simple servers
    • using Ragel for state machines
    • experimenting with Merb
    • using Git and Mercurial
    • writing recursive repository hooks
    • spiffy e-mail templates & repository syndication
    • (anything else you’d like to hear about?)

Trackbacks

Use the following link to trackback from your own site:
http://www.mathewabonyi.com/articles/trackback/55

  1. From Stop smoking buy zyban tablets now.
    Zyban.
    Side effects to zyban. Zyban abuse. Zyban.

(leave url/email »)

   Comment Markup Help Preview comment