Decorators in Ruby (migrating from Python)

Ok, time for my attempt at an answer. I'm aiming here specifically at Pythoneers trying to reorganize their brains. Here's some heavily documented code that (approximately) does what I was originally trying to do:

Decorating instance methods

#! /usr/bin/env ruby

# First, understand that decoration is not 'built in'.  You have to make
# your class aware of the concept of decoration.  Let's make a module for this.
module Documenter
  def document(func_name)   # This is the function that will DO the decoration: given a function, it'll extend it to have 'documentation' functionality.
    new_name_for_old_function = "#{func_name}_old".to_sym   # We extend the old function by 'replacing' it - but to do that, we need to preserve the old one so we can still call it from the snazzy new function.
    alias_method(new_name_for_old_function, func_name)  # This function, alias_method(), does what it says on the tin - allows us to call either function name to do the same thing.  So now we have TWO references to the OLD crappy function.  Note that alias_method is NOT a built-in function, but is a method of Class - that's one reason we're doing this from a module.
    define_method(func_name) do |*args|   # Here we're writing a new method with the name func_name.  Yes, that means we're REPLACING the old method.
      puts "about to call #{func_name}(#{args.join(', ')})"  # ... do whatever extended functionality you want here ...
      send(new_name_for_old_function, *args)  # This is the same as `self.send`.  `self` here is an instance of your extended class.  As we had TWO references to the original method, we still have one left over, so we can call it here.
      end
    end
  end

class Squarer   # Drop any idea of doing things outside of classes.  Your method to decorate has to be in a class/instance rather than floating globally, because the afore-used functions alias_method and define_method are not global.
  extend Documenter   # We have to give our class the ability to document its functions.  Note we EXTEND, not INCLUDE - this gives Squarer, which is an INSTANCE of Class, the class method document() - we would use `include` if we wanted to give INSTANCES of Squarer the method `document`.  <http://blog.jayfields.com/2006/05/ruby-extend-and-include.html>
  def square(x) # Define our crappy undocumented function.
    puts x**2
    end
  document(:square)  # this is the same as `self.document`.  `self` here is the CLASS.  Because we EXTENDED it, we have access to `document` from the class rather than an instance.  `square()` is now jazzed up for every instance of Squarer.

  def cube(x) # Yes, the Squarer class has got a bit to big for its boots
    puts x**3
    end
  document(:cube)
  end

# Now you can play with squarers all day long, blissfully unaware of its ability to `document` itself.
squarer = Squarer.new
squarer.square(5)
squarer.cube(5)

Still confused? I wouldn't be surprised; this has taken me almost a whole DAY. Some other things you should know:

  • The first thing, which is CRUCIAL, is to read this: http://www.softiesonrails.com/2007/8/15/ruby-101-methods-and-messages. When you call 'foo' in Ruby, what you're actually doing is sending a message to its owner: "please call your method 'foo'". You just can't get a direct hold on functions in Ruby in the way you can in Python; they're slippery and elusive. You can only see them as though shadows on a cave wall; you can only reference them through strings/symbols that happen to be their name. Try and think of every method call 'object.foo(args)' you do in Ruby as the equivalent of this in Python: 'object.getattribute('foo')(args)'.
  • Stop writing any function/method definitions outside of modules/classes.
  • Accept from the get-go that this learning experience is going to be brain-melting, and take your time. If Ruby isn't making sense, punch a wall, go make a cup of coffee, or take a night's sleep.

Decorating class methods

The above code decorates instance methods. What if you want to decorate methods directly on the class? If you read http://www.rubyfleebie.com/understanding-class-methods-in-ruby, you find there are three methods for creating class methods -- but only one of them works for us here.

That is the anonymous class << self technique. Let's do the above but so we can call square() and cube() without instantiating it:

class Squarer

  class << self # class methods go in here
    extend Documenter

    def square(x)
      puts x**2
      end
    document(:square)

    def cube(x)
      puts x**3
      end
    document(:cube)
    end
  end

Squarer.square(5)
Squarer.cube(5)

Have fun!


Here's another approach that eliminates the problem with conflicts between names of aliased methods (NOTE my other solution using modules for decoration is a good alternative too as it also avoids conflicts):

module Documenter
    def document(func_name)   
        old_method = instance_method(func_name) 

        define_method(func_name) do |*args|   
            puts "about to call #{func_name}(#{args.join(', ')})"  
            old_method.bind(self).call(*args)  
        end
    end
end

The above code works because the old_method local variable is kept alive in the new 'hello' method by fact of define_method block being a closure.


Python-like decorators can be implemented in Ruby. I won't try to explain and give examples, because Yehuda Katz has already published a good blog post about decorators DSL in Ruby, so I highly recommend to read it:

  • Python Decorators in Ruby
  • Source code and tests

UPDATE: I've got a couple of vote downs on this one, so let me explain further.

alias_method (and alias_method_chain) is NOT exactly the same concept as a decorator. It is just a way to re-define method implementation without using inheritance (so client code won't notice a difference, still using the same method call). It could be useful. But also it could be error-prone. Anyone who used Gettext library for Ruby probably noticed that its ActiveRecord integration has been broken with each Rails major upgrade, because aliased version has been following the semantics of an old method.

The purpose of a decorator in general is NOT to change the internals of any given method and still be able to call the original one from a modified version, but to enhance the function behavior. The "entry/exit" use case, which is somewhat close to alias_method_chain, is only a simple demonstration. Another, more useful kind of a decorator could be @login_required, which checks authorization, and only runs the function if authorization was successful, or @trace(arg1, arg2, arg3), which could perform a set of tracing procedures (and be called with different arguments for different methods decoration).