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