2 min read

Other ways to wrap a method

This is something of a follow-up to Bruce Williams' wrap-up of "wrapping a method" approaches. Bruce covered two approaches:

alias_method_chain: rename the original method, then copy the new method over it and call the old one.

class Foo
  def bar
    1
  end

  alias_method :original_bar, :bar

  def bar
    original_bar + 1
  end
end

super and extend: override #new and extend the new object's metaclass with the method.

class Foo
  def bar
    1
  end

  module Incrementor
    def bar
      super + 1
    end
  end

  def self.new
    super.extend Incrementor
  end
end

I wanted to cover two other options, which come with more serious caveats than Bruce's approaches, but which have their benefits.

explicitly declaring a method as chainable: if you are in control of the code you want to extend, make it chainable from the beginning

class Foo
  chainable = Module.new do
    def bar
      1
    end
  end
  include chainable

  def chainable
    super + 1
  end
end

What's going on above is that instead of declaring the method on the class itself, we're sticking it into an (anonymous) module, then including the module into Foo. As a result, we can now define a method on Foo itself, and super to the module we included. We can even wrap up the idiom pretty simply:

class Module
  def overridable(&blk)
    mod = Module.new(&blk)
    include mod
  end
end

class Foo
  overridable do
    def bar
      1
    end
  end

  def bar
    super + 1
  end
end

I like this approach a lot, but it does require that you have control over the original code. In some cases (for instance, Rails chaining its own code via modules) that is the case. This approach, in my opinion, is superior to alias_method_chain because it does not mutate method names, so it eliminates the confusion of looking for a method from a stack trace on a certain line and finding a method with a different name. Also, having to explicitly declare a method chainable provides a cue to the reader of the code that the method is not complete, and that there might be other components elsewhere.

the best of all worlds (with a caveat): the best option would allow you the benefits of the approach immediately above without requiring that you declare the method chainable (so you could add advice to other peoples' code). The solution, unfortunately, requires a (rather crude) C extension I whipped up.

There are a few things going on above. The interesting part is the little bit of C code. This code lives on any Module. It grabs a method from the module's method table, and inserts it (with the same name) on any other Module. The TrackAddedMethods module can be extended onto any module, and it will remember all the methods that were added (using a method added hook).

The override method uses the same technique as the chainable method above to create a new module with the methods inside of it. Only this time, as the methods get added, we track them. Each method that is added to the module is then removed from the class itself and moved up (via the C extension) to a module, which is then included into the base class. At this point, the base class doesn't have the method on it anymore, but the original method is included into the base class via a module.

At this point, we're at the same place as we would have been using the first technique I described above, so we simply include the module that was created from the override() method and voila: you can tweak someone else's methods without needing to rename them.

I hope the forgoing didn't blow anyone's minds. It blew mine the first time I started playing with it ;)