Weirdness with around aspect in ruby

70 Views Asked by At

I am new to ruby

Trying to write an around aspect. My code is as follows

My code looks as follows

module Utils
  module Aspects

    def self.included(base)
      base.extend(self)
    end

    def around_aspect(method_name, before_proc, after_proc)

      code = %Q[
        def #{method_name} *args, &block
          #{before_proc.call}
          old_#{method_name} *args, &block
          #{after_proc.call}
        end
      ]

      class_eval %Q[
        alias_method :old_#{method_name}, :#{method_name}
      ]

      class_eval code
    end

    # def before_aspect method_name, before_proc
    #   around_aspect method_name, before_proc, ->(){}
    # end
    #
    # def after_aspect method_name, after_proc
    #   around_aspect method_name, ->(){}, after_proc
    # end
  end
end


class Test
  include Utils::Aspects

  def test
    puts 'test'
  end

  before = ->(){puts 'before'}
  after = ->(){puts 'after'}
  around_aspect :test,before,after
end

Test.new.test

The problem is that when i do Test.new.test I expect it to print before, test and after" in order. But right now it prints "before,after and test"

2

There are 2 best solutions below

5
On BEST ANSWER

The problem is that when i do Test.new.test I expect it to print before, test and after" in order. But right now it prints "before,after and test"

No, it doesn't. When calling Test.new.test it only prints test. before and after are printed when defining the wrapped method, i.e. when calling around_advice.

Try to put a puts in between the call to around_advice and the call to Test.new.test (and try to call test several times) to observe this:

puts '______________________'

Test.new.test
Test.new.test

# before
# after
# ______________________
# test
# test

You are calling the lambdas only once, when defining the method:

  code = %Q[
    def #{method_name} *args, &block
      #{before_proc.call}
#     ^^^^^^^^^^^^^^^^^^^
      old_#{method_name} *args, &block
      #{after_proc.call}
#     ^^^^^^^^^^^^^^^^^^
    end
  ]

You need to call them every time when calling the method:

  code = %Q[
    def #{method_name} *args, &block
      before_proc.call
      old_#{method_name} *args, &block
      after_proc.call
    end
  ]

However, it would be much easier to just use Module#prepend, after all, that's what it's there for:

module Aspects
  refine Module do
    def around_aspect(method_name, before_proc, after_proc)
      prepend(Module.new do
        define_method(method_name) do |*args, &block|
          before_proc.()
          super(*args, &block)
          after_proc.()
        end
      end)
    end
  end
end

class Test
  using Aspects

  def test
    puts 'test'
  end

  before = -> {puts 'before'}
  after = -> {puts 'after'}
  around_aspect :test, before, after
end
0
On

Just putting my code up here. This is how i ended up achieving what i was trying to do, module.prepend as suggested above is another way

module Utils
  module Aspects

    def self.included(base)
      base.extend(self)
    end

    def around_aspect(method_name, before_proc, after_proc)

      new_method_name = Random.new_seed.to_s

      alias_method :"#{new_method_name}", :"#{method_name}"

      define_method "#{method_name}" do |*args, &block|
        before_proc.call
        send(:"#{new_method_name}", *args, &block)
        after_proc.call
      end
    end

    def before_aspect method_name, before_proc
      around_aspect method_name, before_proc, ->(){}
    end

    def after_aspect method_name, after_proc
      around_aspect method_name, ->(){}, after_proc
    end
  end
end


class Test
  include Utils::Aspects

  def test

    puts 'test'
  end

  before = ->(){puts 'before'}
  after = ->(){puts 'after'}

  before_aspect :test, before
  after_aspect :test, after
end


Test.new.test