Kurt StephensNerd Up! | ||||||
|
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 It provides simple See Aquarium for more industrial-strength AOP-style programming.
# 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 Comments?
Reply |
||||||
Recent comments
16 hours 59 min ago
2 days 4 hours ago
2 days 23 hours ago
3 days 13 hours ago
5 days 3 hours ago
5 days 6 hours ago
1 week 7 hours ago
1 week 1 day ago
1 week 1 day ago
3 weeks 14 hours ago