Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I run code before and after a method in a sub class?

My first thoughts are some thing like this:

class AbstractBuilder
  attr_reader :time_taken

  def build_with_timer
    started_at = Time.now
    build
    @time_taken = Time.now - started_at
  end

  def build
    raise 'Implement this method in a subclass' 
  end
end

class MyBuilder < AbstractBuilder
  def build
    sleep(5)
  end
end

builder = MyBuilder.new.build_with_timer
puts builder.time_taken

I would suspect there is a better way which offers better flexibility, for example ideally I'd like to call 'build' on an instance of MyBuilder instead of 'build_with_timer' and always have the execution time recorded.

I did consider using alias_method from initialize or even using a module mixin instead of class inheritance which would override the build method calling super in the middle (not sure if that would work). Before I go down the rabbit hole I thought I'd see if there is an established practice.

like image 792
Kris Avatar asked Jul 07 '10 18:07

Kris


2 Answers

I had a stab at a version to achieve what you want. This version doesn't require the subclass to have any extra code either.

class AbstractBuilder

  @@disable_override = false

  def before_method
    puts "before"
  end

  def after_method
    puts "after"
  end

  def self.method_added name
    unless @@disable_override
      if name == :build
        @@disable_override = true # to stop the new build method 
        self.send :alias_method, :sub_build, :build
        self.send :remove_method, :build
        self.send :define_method, :build do
          before_method
          sub_build
          after_method
        end
        @@disable_override = false
      else
        puts "defining other method #{name}"
      end
    end
  end

end

class MyBuilder < AbstractBuilder

  def build
    puts "starting build"
    sleep(5)
    puts "built."
  end

  def unnaffected_method
    # this method won't get redefined
  end

end

b = MyBuilder.new
b.build

Outputs

defining other method unnaffected_method
before
starting build
built.
after
like image 195
Joc Avatar answered Oct 05 '22 12:10

Joc


I'd play with alias_method:

module Timeable
  def time_methods *meths
    meths.each do |meth|
      alias_method "old_#{meth}", meth

      define_method meth do |*args|
        started_at = Time.now
        res = send "old_#{meth}", *args
        puts "Execution took %f seconds" % (Time.now - started_at)
        res
      end
    end
  end

end

class Foo
  def bar str
    puts str
  end
end

Foo.extend Timeable
Foo.time_methods :bar
Foo.new.bar('asd')
#=>asd
#=>Execution took 0.000050 seconds
like image 39
Mladen Jablanović Avatar answered Oct 05 '22 12:10

Mladen Jablanović