Kurt Stephens

Nerd Up!

Recent comments

Syndicate

Syndicate content

Browse archives

« January 2009  
Mo Tu We Th Fr Sa Su
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
Kurt on Fri, 2007-09-07 01:22.

Advice is a programming construct from the Lisp world that pre-dates aspect-oriented and object-oriented programming. Advice is code that is placed before, after or around an existing function’s body. Examples of advice can be seen in Emacs: in Emacs Lisp, the advice is specified with macro syntax that expands to lambdas.

Wrote this (err… something similar at work :) in Ruby just before I found http://aquarium.rubyforge.org. The advice bodies are bound as methods using Class#define_method with a Proc; the advice has access to privates.

It provides simple :before, :after and :around advice objects that can be applied to and removed from multiple instance methods on multiple classes. It’s not AOP because there are no point-cuts patterns, but it is a bit more object-oriented than a typical Lisp advice macro.

See Aquarium for more industrial-strength AOP-style programming.

CheapAdvice:


# Provides cheap advice mechanism for Ruby.
class CheapAdvice
  attr_accessor :before, :after, :around, :advised

  @@advice_id = 0

  NULL_PROC = lambda { | ar | }
  NULL_AROUND_PROC = lambda { | ar, result | result.call }
  EMPTY_HASH = { }.freeze

  # options:
  #   :before
  #   :after
  #   :around
  def initialize opts, &blk
    @advised = [ ]

    opts_hash = EMPTY_HASH
    opts_key = nil

    case opts
      # advice :before => lambda ...
    when Hash 
      opts_hash = opts
      # advice :method, :before do ... end
    when Symbol 
      opts_key = opts
    end

    @before = (opts_key == :before ? blk : opts_hash[:before]) || 
      NULL_PROC
    @after  = (opts_key == :after  ? blk : opts_hash[:after])  || 
      NULL_PROC
    @around = (opts_key == :around ? blk : opts_hash[:around]) || 
      NULL_AROUND_PROC

    @blk = blk
  end

  # Apply advice to class and method.
  def advise cls, method
    return cls.map { | x | advise x, method } if 
      cls.kind_of?(Enumerable)
    return method.map { | x | advise cls, x } if 
      method.kind_of?(Enumerable)

    old_method = "__advice_#{@@advice_id += 1}_#{method}"
    new_method = "__advice_#{@@advice_id += 1}_#{method}"

    before_method = "__advice_before_#{@@advice_id}_#{method}"
    after_method  = "__advice_after_#{@@advice_id}_#{method}"
    around_method = "__advice_around_#{@@advice_id}_#{method}"

    advice = self

    advised = Advised.new(
                          self, cls, 
                          method, old_method, new_method,
                          before_method, after_method, around_method
                          )

    advised.apply_advice_methods

    cls.class_eval do
      define_method advised.new_method do | *args |
        ar = ActivationRecord.new(self, advised.method, args)

        do_result = Proc.new do
          self.send(advised.before_method, ar)
          begin
            ar.result = self.send(advised.old_method, *ar.args)
          rescue Exception => err
            ar.error = err
          ensure
            self.send(advised.after_method, ar)
          end

          ar.result
        end

        self.send advised.around_method, ar, do_result

        raise ar.error if ar.error

        ar.result
      end
    end

    advised.advise

    @advised << advised

    advised
  end

  def unadvise
    @advised.each { | x | x.unadvise }
  end

  def readvise
    @advised.each { | x | x.advise }
  end

  # Represents the application of advice to a class and method.
  class Advised
    attr_reader :advice, :cls
    attr_reader :method, :old_method, :new_method
    attr_reader :before_method, :after_method, :around_method

    def initialize *args
      @advice, @cls, 
      @method, @old_method, @new_method,
      @before_method, @after_method, @around_method = *args
    end

    def apply_advice_methods
      this = self
      @cls.instance_eval do 
        define_method(this.before_method, &this.advice.before)
        define_method(this.after_method,  &this.advice.after)
        define_method(this.around_method, &this.advice.around)  
      end
    end

    def advise
      this = self
      @cls.instance_eval do
        alias_method this.old_method, this.method if
          method_defined? this.method and 
          ! method_defined? this.old_method

        alias_method this.method, this.new_method
      end
    end

    def unadvise
      this = self
      @cls.instance_eval do
        alias_method this.method, this.old_method if
          method_defined? this.old_method
      end
    end
  end

  # Represents the activation record of a method invocation.
  class ActivationRecord
    attr_reader :rcvr, :method, :args
    attr_accessor :result, :error, :body

    def initialize *args
      @rcvr, @method, @args = *args
    end
  end

  ######################################
  # Testing
  #

  class Foo
    attr_accessor :foo

    def baz(arg)
      puts "in baz"
      5 + arg
    end
  end

  class Bar
    attr_accessor :bar

    def baz(arg)
      puts "in baz"
      7 + arg
    end
  end

  def self.test_me

    tracing = CheapAdvice.new(:around) do | ar, result |
      puts "  TRACE: before #{ar.rcvr.class}\##{ar.method}(#{ar.args.join(", ")})"
      puts "         foo = #{@foo.inspect}"
      puts "         bar = #{@bar.inspect}"
      result = result.call
      puts "  TRACE: after  #{ar.rcvr.class}\##{ar.method}(#{ar.args.join(", ")}) => #{result.inspect}"
      ar.result = "yo!"
      puts "  TRACE: return #{ar.result.inspect}"
      "boy!"
    end

    puts "\n With tracing advice:\n"

    tracing.advise( [Foo, Bar], [ :bar, :bar=, :baz ])
    f = Foo.new
    b = Bar.new
    test_do_f(f, b)

    tracing.unadvise
    puts "\n Without tracing advice:\n"
    test_do_f(f, b)

    tracing
  end

  def self.test_do_f(f, b)
    puts "f.foo = 10 => #{(f.foo = 10).inspect}"
    puts "f.foo => #{(f.foo).inspect}"
    puts "f.baz(10) => #{(f.baz(10)).inspect}"

    puts "b.bar = 101 => #{(b.bar = 101).inspect}"
    puts "b.bar => #{(b.bar).inspect}"
    puts "b.baz(10) => #{(b.baz(10)).inspect}"
  end

end

The result:

 > irb -rcheap_advice
irb(main):001:0> CheapAdvice.test_me

 With tracing advice:
f.foo = 10 => 10
f.foo => 10
  TRACE: before CheapAdvice::Foo#baz(10)
         foo = 10
         bar = nil
in baz
  TRACE: after  CheapAdvice::Foo#baz(10) => 15
  TRACE: return "yo!"
f.baz(10) => "yo!"
  TRACE: before CheapAdvice::Bar#bar=(101)
         foo = nil
         bar = nil
  TRACE: after  CheapAdvice::Bar#bar=(101) => 101
  TRACE: return "yo!"
b.bar = 101 => 101
  TRACE: before CheapAdvice::Bar#bar()
         foo = nil
         bar = 101
  TRACE: after  CheapAdvice::Bar#bar() => 101
  TRACE: return "yo!"
b.bar => "yo!"
  TRACE: before CheapAdvice::Bar#baz(10)
         foo = nil
         bar = 101
in baz
  TRACE: after  CheapAdvice::Bar#baz(10) => 17
  TRACE: return "yo!"
b.baz(10) => "yo!"

 Without tracing advice:
f.foo = 10 => 10
f.foo => 10
in baz
f.baz(10) => 15
b.bar = 101 => 101
b.bar => 101
in baz
b.baz(10) => 17
=> nil

With some minimal changes, the CheapAdvice#around, #before, and #after Procs could be changed at run-time, affecting all advised methods.

Comments?

AttachmentSize
cheap_advice.rb5.36 KB
links: read more | Kurt's blog | add new comment | 470 reads | 1 attachment

Reply

The content of this field is kept private and will not be shown publicly.
Captcha Image: you will need to recognize the text in it.
Please type in the letters/numbers that are shown in the image above.