views:

91

answers:

2

I'm trying to add logging to a method from the outside (Aspect-oriented-style)

class A
  def test
    puts "I'm Doing something..."
  end
end

class A # with logging!
  alias_method :test_orig, :test
  def test
    puts "Log Message!"
    test_orig
  end
end

a = A.new
a.test

The above works alright, except that if I ever needed to do alias the method again, it goes into an infinite loop. I want something more like super, where I could extend it as many times as I needed, and each extension with alias its parent.

+2  A: 

First choice: subclass instead of overriding:

class AWithLogging < A\
  def test
    puts "Log Message!"
    super
  end
end

Second choice: name your orig methods more carefully:

class A # with logging!
  alias_method :test_without_logging, :test
  def test
    puts "Log Message!"
    test_without_logging
  end
end

Then another aspect uses a different orig name:

class A # with frobnication!
  alias_method :test_without_frobnication, :test
  def test
    Frobnitz.frobnicate(self)
    test_without_frobnication
  end
end
Grandpa
The thing is, these methods shouldn't need to have a name in the first place. See @rampion's answer for a *much* better solution.
Jörg W Mittag
Yes, I prefer his solution too. Thanks both.
Grandpa
+5  A: 

Another alternative is to use unbound methods:

class A
  original_test = instance_method(:test)
  define_method(:test) do
    puts "Log Message!"
    original_test.bind(self).call
  end
end

class A
  original_test = instance_method(:test)
  counter = 0
  define_method(:test) do
    counter += 1
    puts "Counter = #{counter}"
    original_test.bind(self).call
  end
end

irb> A.new.test
Counter = 1
Log Message!
#=> #....
irb> A.new.test
Counter = 2
Log Message!
#=> #.....

This has the advantage that it doesn't pollute the namespace with additional method names, and is fairly easily abstracted, if you want to make a class method add_logging or what have you.

class Module
  def add_logging(*method_names)
    method_names.each do |method_name|
      original_method = instance_method(method_name)
      define_method(method_name) do |*args,&blk|
        puts "logging #{method_name}"
        original_method.bind(self).call(*args,&blk)
      end
    end
  end
end

class A
  add_logging :test
end

Or, if you wanted to be able to do a bunch of aspects w/o a lot of boiler plate, you could write a method that writes aspect-adding methods!

class Module
  def self.define_aspect(aspect_name, &definition)
    define_method(:"add_#{aspect_name}") do |*method_names|
      method_names.each do |method_name|
        original_method = instance_method(method_name)
        define_method(method_name, &(definition[method_name, original_method]))
      end
    end
  end
  # make an add_logging method
  define_aspect :logging do |method_name, original_method|
    lambda do |*args, &blk|
      puts "Logging #{method_name}"
      original_method.bind(self).call(*args, &blk)
    end
  end
  # make an add_counting method
  global_counter = 0
  define_aspect :counting do |method_name, original_method|
     local_counter = 0
     lambda do |*args, &blk|
       global_counter += 1
       local_counter += 1
       puts "Counters: global@#{global_counter}, local@#{local_counter}"
       original_method.bind(self).call(*args, &blk)
     end
  end      
end

class A
  def test 
    puts "I'm Doing something..." 
  end
  def test1 
    puts "I'm Doing something once..." 
  end
  def test2
    puts "I'm Doing something twice..." 
    puts "I'm Doing something twice..." 
  end
  def test3
    puts "I'm Doing something thrice..." 
    puts "I'm Doing something thrice..." 
    puts "I'm Doing something thrice..." 
  end
  def other_tests
    puts "I'm Doing something else..." 
  end

  add_logging :test, :test2, :test3
  add_counting :other_tests, :test1, :test3
end
rampion
"This has the advantage that it doesn't pollute the namespace with additional method names": Yes! And given that this pollution was the source of the OPs problem in the first place, it's a rather compelling advantage.
Jörg W Mittag
Perfect, thankyou
Sean Clark Hess
Wait... So my original class has to write it's method as unbound in the first place? That's kind of annoying
Sean Clark Hess
@Sean Clark Hess: No, not at all. In the last example all the methods that I add aspects to (test, test1, test2, test3, other_tests) are just normal methods defined in a normal way.
rampion