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.

 1#!/usr/bin/env ruby
 2# Example demonstrating "around" advice that traces calls to all 
 3# methods in classes Foo and Bar.
 4
 5$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
 6require 'aquarium'
 7
 8module Aquarium
 9  class Foo
10    def initialize *args
11      p "Inside:   Foo#initialize: args = #{args.inspect}"
12    end
13    def do_it *args
14      p "Inside:   Foo#do_it: args = #{args.inspect}"
15    end
16  end
17
18  module BarModule
19    def initialize *args
20      p "Inside:   BarModule#initialize: args = #{args.inspect}"
21    end
22    def do_something_else *args
23      p "Inside:   BarModule#do_something_else: args = #{args.inspect}"
24    end
25  end
26
27  class Bar
28    include BarModule
29  end
30end
31
32p "Before advising the methods:"
33foo1 = Aquarium::Foo.new :a1, :a2
34foo1.do_it :b1, :b2
35
36bar1 = Aquarium::Bar.new :a3, :a4
37bar1.do_something_else :b3, :b4
38
39include Aquarium::Aspects
40
41# "jp" is the "join point", a.k.a. the current execution point.
42Aspect.new :around, :calls_to => :all_methods, 
43    :for_types => [Aquarium::Foo, Aquarium::Bar],
44    :method_options => :exclude_ancestor_methods do |jp, obj, *args|
45  begin
46    names = "#{jp.target_type.name}##{jp.method_name}"
47    p "Entering: #{names}: args = #{args.inspect}"
48    jp.proceed
49  ensure
50    p "Leaving:  #{names}: args = #{args.inspect}"
51  end
52end
53
54p "After advising the methods. Notice that #intialize isn't advised:"
55foo2 = Aquarium::Foo.new :a5, :a6
56foo2.do_it :b5, :b6
57
58bar1 = Aquarium::Bar.new :a7, :a8
59bar1.do_something_else :b7, :b8
60
61# The "begin/ensure/end" idiom shown causes the advice to return the correct
62# value; the result of the "proceed", rather than the value returned by "p"!
63Aspect.new :around, :invocations_of => :initialize, 
64    :for_types => [Aquarium::Foo, Aquarium::Bar],  
65    :restricting_methods_to => :private_methods do |jp, obj, *args|
66  begin
67    names = "#{jp.target_type.name}##{jp.method_name}"
68    p "Entering: #{names}: args = #{args.inspect}"
69    jp.proceed
70  ensure
71    p "Leaving:  #{names}: args = #{args.inspect}"
72  end
73end
74
75p "After advising the private methods. Notice that #intialize is advised:"
76foo2 = Aquarium::Foo.new :a9, :a10
77foo2.do_it :b9, :b10
78
79bar1 = Aquarium::Bar.new :a11, :a12
80bar1.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:

 1#!/usr/bin/env ruby
 2# Example demonstrating "around" advice for method_missing. This is a 
 3# technique for avoiding collisions when different toolkits want to override
 4# method_missing in the same classes, e.g., Object. Using around advice as 
 5# shown allows a toolkit to add custom behavior while invoking the "native" 
 6# method_missing to handle unrecognized method calls.
 7# Note that it is essential to use around advice, not before or after advice,
 8# because neither can prevent the call to the "wrapped" method_missing, which
 9# is presumably not what you want. In this (contrived) example, an Echo class
10# uses method_missing to simply echo the method name and arguments. An aspect
11# is used to intercept any calls to a fictitious "log" method and handle those
12# in a different way.
13
14$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
15require 'aquarium'
16
17module Aquarium
18  class Echo
19    def method_missing sym, *args
20      p "Echoing: #{sym.to_s}: #{args.join(" ")}"
21    end
22    def respond_to? sym, include_private = false
23      true
24    end
25  end
26end
27
28p "Before advising Echo:"
29echo1 = Aquarium::Echo.new
30echo1.say "hello", "world!"
31echo1.log "something", "interesting..."
32echo1.shout "theater", "in", "a", "crowded", "firehouse!"
33
34Aquarium::Aspects::Aspect.new :around, 
35    :calls_to => :method_missing, 
36    :for_type => Aquarium::Echo do |jp, obj, sym, *args|
37  if sym == :log 
38    p "--- Sending to log: #{args.join(" ")}" 
39  else
40    jp.proceed
41  end
42end
43
44p "After advising Echo:"
45echo2 = Aquarium::Echo.new
46echo2.say "hello", "world!"
47echo2.log "something", "interesting..."
48echo2.shout "theater", "in", "a", "crowded", "firehouse!"
49
50

“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.

 1#!/usr/bin/env ruby
 2# Example demonstrating "wrapping" an exception; rescuing an exception and 
 3# throwing a different one. A common use for this is to map exceptions across
 4# "domain" boundaries, e.g., persistence and application logic domains. 
 5# Note that you must use :around advice, since :after_raising cannot change
 6# the control flow.
 7# (However, see feature request #19119)
 8
 9$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
10require 'aquarium'
11
12module Aquarium
13  class Exception1 < Exception; end
14  class Exception2 < Exception; end
15  class NewException < Exception; end
16
17  class Raiser
18    def raise_exception1
19      raise Exception1.new("one")
20    end
21    def raise_exception2
22      raise Exception2.new("two")
23    end
24  end
25end
26
27Aquarium::Aspects::Aspect.new :around, 
28    :calls_to => /^raise_exception/, 
29    :in_type => Aquarium::Raiser do |jp, obj, *args|
30  begin
31    jp.proceed
32  rescue Aquarium::Exception1 => e
33    raise Aquarium::NewException.new("Exception message was \"#{e.message}\"")
34  end
35end
36
37p "The raised Aquarium::Exception2 raised here won't be intercepted:"
38begin
39  Aquarium::Raiser.new.raise_exception2
40rescue Aquarium::Exception2 => e
41  p "Rescued exception: #{e.class} with message: #{e}"
42end
43
44p "The raised Aquarium::Exception1 raised here will be intercepted and"
45p " Aquarium::NewException will be raised:"
46begin
47  Aquarium::Raiser.new.raise_exception1
48rescue Aquarium::NewException => e
49  p "Rescued exception: #{e.class} with message: #{e}"
50end
51

“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!

 1#!/usr/bin/env ruby
 2# Example demonstrating a hack for defining a reusable aspect in a module
 3# so that the aspect only gets created when the module is included by another
 4# module or class.
 5# Hacking like this defies the spirit of Aquarium's goal of being "intuitive",
 6# so I created a feature request #19122 to address this problem.
 7#
 8# WARNING: put the "include ..." statement at the END of the class declaration,
 9# as shown below. If you put the include statement at the beginning, as you
10# normally wouuld for including a module, it won't advice any join points, 
11# because no methods will have been defined at that point!!
12 
13$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
14require 'aquarium'
15
16module Aquarium
17  module Reusables
18    module TraceMethods
19      def self.append_features mod
20        Aquarium::Aspects::Aspect.new :around, :type => mod, 
21            :methods => :all, 
22            :method_options => [:exclude_ancestor_methods] do |jp, object, *args|
23          names = "#{jp.target_type.name}##{jp.method_name}"
24          p "Entering: #{names}: args = #{args.inspect}"
25          jp.proceed
26          p "Leaving:  #{names}: args = #{args.inspect}"
27        end
28      end
29    end    
30  end
31end
32
33class NotTraced1
34  def doit; p "NotTraced1#doit"; end
35end
36p "You will be warned that no join points in NotTraced2 were matched."
37p "This happens because the include statement and hence the aspect evaluation"
38p " happen BEFORE any methods are defined!"
39class NotTraced2
40  include Aquarium::Reusables::TraceMethods
41  def doit; p "NotTraced2#doit"; end
42end
43class Traced1
44  def doit; p "Traced1#doit"; end
45  include Aquarium::Reusables::TraceMethods
46end
47class Traced2
48  def doit; p "Traced1#doit"; end
49  include Aquarium::Reusables::TraceMethods
50end
51
52p ""
53p "No method tracing:"
54NotTraced1.new.doit
55NotTraced1.new.doit
56p ""
57p "Method tracing:"
58Traced1.new.doit
59Traced2.new.doit
60

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.

 1#!/usr/bin/env ruby
 2# Example demonstrating emerging ideas about good aspect-oriented design. 
 3# Specifically, this example follows ideas of Jonathan Aldrich on 
 4# "Open Modules", where a "module" (in the generic sense of the word...) is
 5# responsible for defining and maintaining the pointcuts that it is willing
 6# to expose to potential aspects. Aspects are only allowed to advise the 
 7# module through the pointcut. (Enforcing this constraint is TBD.) Griswold, 
 8# Sullivan, and collaborators have expanded on these ideas. See their IEEE
 9# Software, March 2006 paper.
10
11$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
12require 'aquarium'
13
14module Aquarium
15  class ClassWithStateAndBehavior
16    include Aquarium::DSL
17    def initialize *args
18      @state = args
19      p "Initializing: #{args.inspect}"
20    end
21    attr_accessor :state
22  
23    # Two alternative versions of the following pointcut would be 
24    # STATE_CHANGE = pointcut :method => :state=
25    # STATE_CHANGE = pointcut :attribute => :state, :attribute_options => [:writers]
26    # Note that only matching on the attribute writers is important, especially
27    # given the advice block below, because if the reader is allowed to be 
28    # advised, we get an infinite recursion of advice invocation! The correct 
29    # solution is the planned extension of the pointcut language to support 
30    # condition tests for context. I.e., we don't want the advice applied when
31    # it's already inside advice.
32    STATE_CHANGE = pointcut :writing => :state
33  end
34end
35
36include Aquarium::Aspects
37
38# Observe state changes in the class, using the class-defined pointcut.
39# Two ways of referencing the pointcut are shown. The first assumes you know
40# the particular pointcuts you care about. The second is more general; it 
41# uses the recently-introduced :named_pointcut feature to search for all
42# pointcuts matching a name in a set of types.
43
44observer1 = Aspect.new :after, 
45  :pointcut => Aquarium::ClassWithStateAndBehavior::STATE_CHANGE do |jp, obj, *args|
46  p "State has changed. "
47  state = obj.state
48  p "  New state is #{state.nil? ? 'nil' : state.inspect}"
49  p "  Equivalent to *args: #{args.inspect}"
50end  
51
52observer2 = Aspect.new :after, :named_pointcuts => {:matching => /CHANGE/, 
53    :within_types => Aquarium::ClassWithStateAndBehavior} do |jp, obj, *args|
54  p "State has changed. "
55  state = obj.state
56  p "  New state is #{state.nil? ? 'nil' : state.inspect}"
57  p "  Equivalent to *args: #{args.inspect}"
58end  
59
60object = Aquarium::ClassWithStateAndBehavior.new(:a1, :a2, :a3)
61object.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 Aquarium::Extras, which this example uses:

 1#!/usr/bin/env ruby
 2# Example demonstrating "Design by Contract", Bertrand Meyer's idea for
 3# programmatically-specifying the contract of use for a class or module and
 4# testing it at runtime (usually during the testing process). This example 
 5# is adapted from spec/extras/design_by_contract_spec.rb.
 6# Note: the DesignByContract module adds the #precondition, #postcondition, 
 7# and #invariant methods shown below to Object and they use "self" as the 
 8# :object to advise.  
 9 
10$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
11require 'aquarium/extras/design_by_contract'
12
13module Aquarium
14  class PreCond
15    def action *args
16      p "inside :action"
17    end
18  
19    precondition :calls_to => :action, 
20        :message => "Must pass more than one argument." do |jp, obj, *args|
21      args.size > 0
22    end
23  end
24end
25  
26p "This call will fail because the precondition is not satisfied:"
27begin
28  Aquarium::PreCond.new.action
29rescue Aquarium::Extras::DesignByContract::ContractError => e
30  p e.inspect
31end
32p "This call will pass because the precondition is satisfied:"
33Aquarium::PreCond.new.action :a1
34
35module Aquarium
36  class PostCond
37    def action *args
38      args.empty? ? args.dup : args + [:a]
39    end
40  
41    postcondition :calls_to => :action, 
42      :message => "Must return new array, [:a] + args." do |jp, obj, *args|
43      jp.context.returned_value.size == args.size + 1 && 
44      jp.context.returned_value[-1] == :a
45    end
46  end
47end
48
49p "These two calls will fail because the postcondition is not satisfied:"
50begin
51  Aquarium::PostCond.new.action
52rescue Aquarium::Extras::DesignByContract::ContractError => e
53  p e.inspect
54end
55p "This call will pass because the postcondition is satisfied:"
56Aquarium::PostCond.new.action  :x1, :x2
57
58module Aquarium
59  class InvarCond
60    def initialize 
61      @invar = 0
62    end
63    attr_reader :invar
64    def good_action
65      p "inside :good_action"
66    end
67    def bad_action
68      p "inside :bad_action"
69      @invar = 1
70    end
71  
72    invariant :calls_to => /action$/, 
73        :message => "The @invar value must not change." do |jp, obj, *args|
74      obj.invar == 0
75    end
76  end
77end
78
79p "This call will fail because the invariant is not satisfied:"
80begin
81  Aquarium::InvarCond.new.bad_action
82rescue Aquarium::Extras::DesignByContract::ContractError => e
83  p e.inspect
84end
85p "This call will pass because the invariant is satisfied:"
86Aquarium::InvarCond.new.good_action
87

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:

 1$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
 2require 'aquarium'
 3
 4module Aquarium
 5  module TypeFinderIntroductionExampleTargetModule1
 6  end
 7  module TypeFinderIntroductionExampleTargetModule2
 8  end
 9  class TypeFinderIntroductionExampleTargetClass1
10  end
11  class TypeFinderIntroductionExampleTargetClass2
12  end
13  module TypeFinderIntroductionExampleModule
14    def introduced_method; end
15  end
16end
17
18include Aquarium::Finders
19
20# First, find the types
21
22found = TypeFinder.new.find :types => /Aquarium::TypeFinderIntroductionExampleTarget/
23
24# Now, iterate through them and "extend" them with the module defining 
25# the new behavior.
26
27found.each {|t| t.extend Aquarium::TypeFinderIntroductionExampleModule }
28
29# See if the "introduced" modules's method is there.
30
31[Aquarium::TypeFinderIntroductionExampleTargetModule1, 
32 Aquarium::TypeFinderIntroductionExampleTargetModule2,
33 Aquarium::TypeFinderIntroductionExampleTargetClass1,
34 Aquarium::TypeFinderIntroductionExampleTargetClass2].each do |t|
35   p "type #{t}, method there? #{t.methods.include?("introduced_method")}"
36end
37

V0.7.3
  • gem install aquarium
  • gem update aquarium

Menu

Other Links