Charly's Tech Blog

home

From Decorator Pattern to Before Filter 2

16 Sep 2008

In my previous post From Decorator Pattern to Before Filter 1 we got to the point where we could write this:

coffee = Coffee.new
coffee.mixin Callback
coffee.before_filter :spoon_required, :cost
coffee.cost

Now the thing missing is that we're only filtering one method ('cost' in this case) wheras the before_filter in rails filters all methods by default. So how do we do this?

Quit easily, we just have to list all the instance methods inside the class Coffee (ruby provides this possiblity with "instance_methods(false)").... and... iterate!:

require 'rubygems'
require 'mixology'

module Callback
  def filter_with filter, filtered_method
    Callback.method_factory(filter, filtered_method)
  end

  def before_filter filter
    methods = self.class.instance_methods(false)
    methods.delete(filter.to_s)
    methods.each do |filtered_method|
      filter_with filter, filtered_method
    end
  end


  def self.method_factory(filter, filtered_method)
    define_method(filtered_method) do
      send filter
      super
    end
  end
end

class Coffee
  def spoon_required
    puts "get a spoon to steer sugar || creme"
  end

  def cost
    puts "3"
  end

  def smell
    puts "good"
  end

  def color
    puts "black"
  end
end

coffee = Coffee.new
coffee.mixin Callback
coffee.before_filter :spoon_required

# Every call beneath is prepended by :
#  - "get a spoon to steer sugar || creme" 
coffee.cost
coffee.smell
coffee.color

After that adding options like :except=>"smell" is easy. Dealing with inheritance is probably less obvious. The one main differance we have with the rails version of before_filter is the fact that it is set inside the classes body. Not at object creation, and for a good reason since controllers instances are created under the hood.

Of course the easy path is adding: extend(Creme) and before_filter(:spoon_required) in the Coffee's initialize method, but my question was: "is it possible to make it look like this" :

class Coffee
  include Creme
  before_filter :spoon_required

  def spoon_required
    puts "get spoon"
  end

  def cost
    puts "3"
  end
end

The answer is yes although it's a bit convoluted (it looks like a rail plugin with module InstanceMethod and so on) AND we still have to overide initialize somewhere which makes it dangerous.

Now one word of explanation: the hole elegance of the Decorator Pattern in ruby relies on the fact we're calling super instead of aliasing twice the method we're decorating (or unbinding and binding). Calling super() is possible because when we extend an object with a module (like in coffee.extend Creme) we're actually including the module in the ghostclass (or metaclass or singleton class) which sits right underneath the 'real' class in the inheritance chain.

(TODO add figure)

So wouldn't be nice if we could mixin a module 'underneath' a class by saying so in the body of the class so we could easily do all that aspect oriented stuff without having to worry about it at instanciation time ?

The great Library Facets, gives it a try with prepend which can be used this way :

require 'rubygems'
require 'facets'

module Creme  
  def cost
    puts "before_filter"
    super
  end
end

class Coffee
  prepend Creme

  def cost
    puts "3"
  end
end

coffee = Coffee.new
coffee.cost

#But guess what, it's by overriding new:

# facet library
  def prepend( aspect )
    _new      = method(:new)
    _allocate = method(:allocate)
    (class << self; self; end).class_eval do
      define_method(:new) do |*args|
        o = _new.call(*args)
        o.extend aspect
        o
      end
      define_method(:allocate) do |*args|
        o = _allocate.call(*args)
        o.extend aspect
        o
      end
    end
  end

Comments welcome !