views:

91

answers:

4

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.

A: 

Sounds like you're looking for hooks into object lifecycle events. You'll have to build this into your base object and provide a little DSL -- I'm thinking you're after something like ActiveRecord Callbacks. Here's how we might modify your example to allow something like that:

class AbstractBuilder
  attr_reader :time_taken

  def construct! # i.e., build, and also call your hooks
    @@prebuild.each { |sym| self.send(sym) }
    build
    @@postbuild.each { |sym| self.send(sym) }
  end  

  def construct_with_timer
    started_at = Time.now
    construct!
    @time_taken = Time.now - started_at

    puts "!!! Build time: #@time_taken"
  end

  class << self
    def before_build(fn); @@prebuild ||= []; @@prebuild << fn; end
    def after_build(fn);  @@postbuild ||= []; @@postbuild << fn; end
  end
end

class MyBuilder < AbstractBuilder
  before_build :preprocess
  after_build  :postprocess

  def build; puts "BUILDING"; sleep(3); end
  def preprocess;  puts "Preparing to build..."; end
  def postprocess; puts "Done building. Thank you for waiting."; end
end

builder = MyBuilder.new
builder.construct_with_timer

# => Preparing to build...
# => BUILDING
# => Done building. Thank you for waiting.
# => !!! Build time: 3.000119
Joe
Yes and no, I want the concept of before/after filters (or around) but there is only ever going to be one method, always with the same name, that I need to wrap, and it will always be wrapped in the same code. So I kind of need a hard coded version of this. From you example it looks like I need to store the before/after code at the class level, then inject it after the object has been instantiated?
Kris
Right -- the before/after logic is a little language for quickly building child classes. For more complicated requirements, note that Ruby itself also provides some internal callbacks here that are helpful. You can write methods named 'inherited' and 'included' and these will be fired whenever the class is inherited or included.
Joe
+1  A: 

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
Mladen Jablanović
This looks like it could work, I don't want to have to call time_methods since in my case the is one and only one method in the subclass, always of the same name, which I want to wrap code around. I will try using alias_method and define_method from initialize in the base class. That is assuming that the methods in the subclass exist when initialize is being called?!?
Kris
You've lost me there. It would help if you would describe what real problem you are trying to solve. For start, do you have control of the parent class? Can you change `build_with_timer` method or not?
Mladen Jablanović
Yes I control both classes, there will be a convention of having a method called 'build' in the subclasses, which when called with have code specified in the base class run before and after the method, the before/after code will always be the same. Hope that makes sense.
Kris
+3  A: 

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
Joc
A: 

This is a textbook-definition use case for Aspect-Oriented Programming. It generally offers a cleaner separation of concerns. In this arena, Ruby offers Aquarium and AspectR. However, you may not want to add another dependency to your project. As such, you might still consider using one of the other approaches.

Mario