Examples

These examples are all in the “examples” directory that is part of the distribution. Other examples can be found in the README, in particular, different ways of expressing the same ideas (for readability…).

Tracing Method Calls

Want to trace invocations of certain methods? This example demonstrates how to do it.

#!/usr/bin/env ruby
# Example demonstrating "around" advice that traces calls to all methods in
# classes Foo and Bar

$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
require 'aquarium'

module Aquarium
  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 = Aquarium::Foo.new :a1, :a2
foo1.do_it :b1, :b2

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

include Aquarium::Aspects

Aspect.new :around, :calls_to => :all_methods, :for_types => [Aquarium::Foo, Aquarium::Bar],
    :method_options => :exclude_ancestor_methods do |execution_point, obj, *args|
  begin
    p "Entering: #{execution_point.target_type.name}##{execution_point.method_name}: args = #{args.inspect}"
    execution_point.proceed
  ensure
    p "Leaving:  #{execution_point.target_type.name}##{execution_point.method_name}: args = #{args.inspect}"
  end
end

p "After advising the methods. Notice that #intialize isn't advised:"
foo2 = Aquarium::Foo.new :a5, :a6
foo2.do_it :b5, :b6

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

# The "begin/ensure/end" idiom shown causes the advice to return the correct value; the result
# of the "proceed", rather than the value returned by "p"!
Aspect.new :around, :invocations_of => :initialize, :for_types => [Aquarium::Foo, Aquarium::Bar],  
    :restricting_methods_to => :private_methods do |execution_point, obj, *args|
  begin
    p "Entering: #{execution_point.target_type.name}##{execution_point.method_name}: args = #{args.inspect}"
    execution_point.proceed
  ensure
    p "Leaving:  #{execution_point.target_type.name}##{execution_point.method_name}: args = #{args.inspect}"
  end
end

p "After advising the private methods. Notice that #intialize is advised:"
foo2 = Aquarium::Foo.new :a9, :a10
foo2.do_it :b9, :b10

bar1 = Aquarium::Bar.new :a11, :a12
bar1.do_something_else :b11, :b12

“Enhancing” method_missing Without Overriding

Here is an example of “advising” method_missing to add behavior, rather than overriding it, which can increase the risk of collisions with overrides from other toolkits:

#!/usr/bin/env ruby
# Example demonstrating "around" advice for method_missing. This is a technique for
# avoiding collisions when different toolkits want to override method_missing in the
# same classes, e.g., Object. Using around advice as shown allows a toolkit to add 
# custom behavior while invoking the "native" method_missing to handle unrecognized
# method calls.
# Note that it is essential to use around advice, not before or after advice, because
# neither can prevent the call to the "wrapped" method_missing, which is presumably
# not what you want.
# In this (contrived) example, an Echo class uses method_missing to simply echo
# the method name and arguments. An aspect is used to intercept any calls to a 
# fictitious "log" method and handle those in a different way.

$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
require 'aquarium'

module Aquarium
  class Echo
    def method_missing sym, *args
      p "Echoing: #{sym.to_s}: #{args.join(" ")}"
    end
    def respond_to? sym, include_private = false
      true
    end
  end
end

p "Before advising Echo:"
echo1 = Aquarium::Echo.new
echo1.say "hello", "world!"
echo1.log "something", "interesting..."
echo1.shout "theater", "in", "a", "crowded", "firehouse!"

Aquarium::Aspects::Aspect.new :around, 
  :calls_to => :method_missing, :for_type => Aquarium::Echo, do |join_point, obj, sym, *args|
  if sym == :log 
    p "--- Sending to log: #{args.join(" ")}" 
  else
    join_point.proceed
  end
end

p "After advising Echo:"
echo2 = Aquarium::Echo.new
echo2.say "hello", "world!"
echo2.log "something", "interesting..."
echo2.shout "theater", "in", "a", "crowded", "firehouse!"

“Wrapping” an Exception: Rescuing one type and raising another

While it’s tempting to try this with :after_raising advice, this won’t work, because you can’t change the control flow with any form of advice, except for :around advice. (See, however, feature request #19119.) Here is the idiom to use.

#!/usr/bin/env ruby
# Example demonstrating "wrapping" an exception; rescuing an exception and 
# throwing a different one. A common use for this is to map exceptions across
# "domain" boundaries, e.g., persistence and application logic domains. 
# Note that you must use :around advice, since :after_raising cannot change
# the control flow.
# (However, see feature request #19119)

$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
require 'aquarium'

module Aquarium
  class Exception1 < Exception; end
  class Exception2 < Exception; end
  class NewException < Exception; end

  class Raiser
    def raise_exception1
      raise Exception1.new("one")
    end
    def raise_exception2
      raise Exception2.new("two")
    end
  end
end

Aquarium::Aspects::Aspect.new :around, 
  :calls_to => /^raise_exception/, :in_type => Aquarium::Raiser do |jp, obj, *args|
  begin
    jp.proceed
  rescue Aquarium::Exception1 => e
    raise Aquarium::NewException.new("Old exception message was \"#{e.message}\"")
  end
end

p "The raised Aquarium::Exception2 raised here won't be intercepted:"
begin
  Aquarium::Raiser.new.raise_exception2
rescue Aquarium::Exception2 => e
  p "Rescued exception: #{e.class} with message: #{e}"
end

p "The raised Aquarium::Exception1 raised here will be intercepted and Aquarium::NewException will be raised:"
begin
  Aquarium::Raiser.new.raise_exception1
rescue Aquarium::NewException => e
  p "Rescued exception: #{e.class} with message: #{e}"
end

“Hack” to Create a Reusable Aspect that Is Defined When a Module is Included in Another

It’s actually harder than it should be to define a reusable aspect, because the aspect is evaluated when it’s defined, which means the pointcut needs to be known at that time. Feature request #19122 will address this issue. For now, here’s a hack that works around the limitation. Make sure you heed the warning!

#!/usr/bin/env ruby
# Example demonstrating a hack for defining a reusable aspect in a module
# so that the aspect only gets created when the module is included by another
# module or class.
# Hacking like this defies the spirit of Aquarium's goal of being "intuitive",
# so I created a feature request #19122 to address this problem.
#
# WARNING: put the "include ..." statement at the END of the class declaration,
# as shown below. If you put the include statement at the beginning, as you
# normally wouuld for including a module, it won't advice any join points, 
# because no methods will have been defined at that point!!
 
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
require 'aquarium'

module Aquarium
  module Reusables
    module TraceMethods
      def self.append_features mod
        Aquarium::Aspects::Aspect.new :around, 
            :type => mod, :methods => :all, :method_options => [:exclude_ancestor_methods] do |jp, object, *args|
          p "Entering: "+jp.target_type.name+"#"+jp.method_name.to_s+": args = "+args.inspect
          jp.proceed
          p "Leaving:  "+jp.target_type.name+"#"+jp.method_name.to_s+": args = "+args.inspect
        end
      end
    end    
  end
end

class NotTraced1
  def doit; p "NotTraced1#doit"; end
end
p "You will be warned that no join points in NotTraced2 were matched."
p "This happens because the include statement and hence the aspect evaluation happen BEFORE any methods are defined!"
class NotTraced2
  include Aquarium::Reusables::TraceMethods
  def doit; p "NotTraced2#doit"; end
end
class Traced1
  def doit; p "Traced1#doit"; end
  include Aquarium::Reusables::TraceMethods
end
class Traced2
  def doit; p "Traced1#doit"; end
  include Aquarium::Reusables::TraceMethods
end

p ""
p "No method tracing:"
NotTraced1.new.doit
NotTraced1.new.doit
p ""
p "Method tracing:"
Traced1.new.doit
Traced2.new.doit

Aspect Design

The AOP community is still discovering good design principles. However, simple extensions of good OOD principles are an important step. Many of those principles focus on minimal coupling between components and, in particular, coupling through abstractions, rather than concrete details.

In this example, we demonstrate how one module defines a pointcut representing an object’s state changes. An “observer” aspect watches for those changes and prints a message when updates occur. Hence, the example demonstrates one implementation of the Observer Pattern.

Notice how the aspect is independent of the details of the pointcut, so it won’t require change as the observed class evolves.

#!/usr/bin/env ruby
# Example demonstrating emerging ideas about good aspect-oriented design. Specifically, this 
# example follows ideas of Jonathan Aldrich on "Open Modules", where a "module" (in the generic
# sense of the word...) is responsible for defining and maintaining the pointcuts that it is 
# willing to expose to potential aspects. Aspects are only allowed to advise the module through
# the pointcut. (Enforcing this constraint is TBD)
# Griswold, Sullivan, and collaborators have expanded on these ideas. See their IEEE Software,
# March 2006 paper.

$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
require 'aquarium'

module Aquarium
  class ClassWithStateAndBehavior
    include Aquarium::DSL
    def initialize *args
      @state = args
      p "Initializing: #{args.inspect}"
    end
    attr_accessor :state
  
    # Two alternative versions of the following pointcut would be 
    # STATE_CHANGE = pointcut :method => :state=
    # STATE_CHANGE = pointcut :attribute => :state, :attribute_options => [:writers]
    # Note that only matching on the attribute writers is important, especially
    # given the advice block below, because if the reader is allowed to be advised,
    # we get an infinite recursion of advice invocation! The correct solution is
    # the planned extension of the pointcut language to support condition tests for
    # context. I.e., we don't want the advice applied when it's already inside advice.
    STATE_CHANGE = pointcut :writing => :state
  end
end

include Aquarium::Aspects

# Observe state changes in the class, using the class-defined pointcut.
# Two ways of referencing the pointcut are shown. The first assumes you know the particular
# pointcuts you care about. The second is more general; it uses the recently-introduced
# :named_pointcut feature to search for all pointcuts matching a name in a set of types.

observer1 = Aspect.new :after, 
  :pointcut => Aquarium::ClassWithStateAndBehavior::STATE_CHANGE do |jp, obj, *args|
  p "State has changed. "
  state = obj.state
  p "  New state is #{state.nil? ? 'nil' : state.inspect}"
  p "  Equivalent to *args: #{args.inspect}"
end  

observer2 = Aspect.new :after, :named_pointcuts => {:matching => /CHANGE/, 
    :within_types => Aquarium::ClassWithStateAndBehavior} do |jp, obj, *args|
  p "State has changed. "
  state = obj.state
  p "  New state is #{state.nil? ? 'nil' : state.inspect}"
  p "  Equivalent to *args: #{args.inspect}"
end  

object = Aquarium::ClassWithStateAndBehavior.new(:a1, :a2, :a3)
object.state = [:b1, :b2]

Design by Contract

It is easy to use Aquarium to implement a “contract” for a Module, in the sense of Bertrand Meyer’s “Design by Contract”. In fact, there is a simple DbC module (not a complete implementation…) in the extras, which this example uses:

#!/usr/bin/env ruby
# Example demonstrating "Design by Contract", Bertrand Meyer's idea for programmatically-
# specifying the contract of use for a class or module and testing it at runtime (usually
# during the testing process)
# This example is adapted from spec/extras/design_by_contract_spec.rb.
# Note: the DesignByContract module adds the #precondition, #postcondition, and #invariant
# methods shown below to Object and they use "self" as the :object to advise.  
 
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
require 'aquarium/extras/design_by_contract'

module Aquarium
  class PreCond
    def action *args
      p "inside :action"
    end
  
    precondition :calls_to => :action, :message => "Must pass more than one argument." do |jp, obj, *args|
      args.size > 0
    end
  end
end
  
p "This call will fail because the precondition is not satisfied:"
begin
  Aquarium::PreCond.new.action
rescue Aquarium::Extras::DesignByContract::ContractError => e
  p e.inspect
end
p "This call will pass because the precondition is satisfied:"
Aquarium::PreCond.new.action :a1

module Aquarium
  class PostCond
    def action *args
      args.empty? ? args.dup : args + [:a]
    end
  
    postcondition :calls_to => :action, 
      :message => "Must return a copy of the input args with :a appended to it." do |jp, obj, *args|
      jp.context.returned_value.size == args.size + 1 && jp.context.returned_value[-1] == :a
    end
  end
end

p "These two calls will fail because the postcondition is not satisfied:"
begin
  Aquarium::PostCond.new.action
rescue Aquarium::Extras::DesignByContract::ContractError => e
  p e.inspect
end
p "This call will pass because the postcondition is satisfied:"
Aquarium::PostCond.new.action  :x1, :x2

module Aquarium
  class InvarCond
    def initialize 
      @invar = 0
    end
    attr_reader :invar
    def good_action
      p "inside :good_action"
    end
    def bad_action
      p "inside :bad_action"
      @invar = 1
    end
  
    invariant :calls_to => /action$/, :message => "Must not change the @invar value." do |jp, obj, *args|
      obj.invar == 0
    end
  end
end

p "This call will fail because the invariant is not satisfied:"
begin
  Aquarium::InvarCond.new.bad_action
rescue Aquarium::Extras::DesignByContract::ContractError => e
  p e.inspect
end
p "This call will pass because the invariant is satisfied:"
Aquarium::InvarCond.new.good_action

Other examples can be found in the README.

Testing Uses of Aspects

Aspects are sometimes used for “fault injection”, to enable easier testing of error handling logic, and for stubbing expensive methods, using around advice. The latter technique complements mocking frameworks. You can see an example of stubbing the expensive TypeUtils#descendents in Aquarium::TypeUtilsStub (in spec_example_types.rb) and the use of it in spec/aquarium/aspects/pointcut_spec.rb.

Mimicking Introductions (a.k.a. Intertype Declarations)

AspectJ lets Java programmers add new methods and attributes to types. Ruby makes this easy however, so Aquarium doesn’t provide a similar facility. However, if you need to extend a set of types, Aquarium’s TypeFinder can be helpful, as shown here:

$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
require 'aquarium'

module Aquarium
  module TypeFinderIntroductionExampleTargetModule1
  end
  module TypeFinderIntroductionExampleTargetModule2
  end
  class TypeFinderIntroductionExampleTargetClass1
  end
  class TypeFinderIntroductionExampleTargetClass2
  end
  module TypeFinderIntroductionExampleModule
    def introduced_method; end
  end
end

include Aquarium::Finders

# First, find the types

found = TypeFinder.new.find :types => /Aquarium::TypeFinderIntroductionExampleTarget/

# Now, iterate through them and "extend" them with the module defining the new behavior.

found.each {|t| t.extend Aquarium::TypeFinderIntroductionExampleModule }

# See if the "introduced" modules's method is there.

[Aquarium::TypeFinderIntroductionExampleTargetModule1, 
 Aquarium::TypeFinderIntroductionExampleTargetModule2,
 Aquarium::TypeFinderIntroductionExampleTargetClass1,
 Aquarium::TypeFinderIntroductionExampleTargetClass2].each do |t|
   p "type #{t}, method there? #{t.methods.include?("introduced_method")}"
end