Milhouse on software, engineering, and Emacs.

Module prepend as around advices

TL;DR: In this post I will show how you can achieve something like aspect oriented programming’s (AOP) around advice by using plain Ruby modules. I will show examples where this technique can be used to DRY out your code by centralizing cross-cutting concerns.

Introduction

AOP can be extremely useful for DRY-ing cross-cutting concerns from your code. Such concerns are things like logging, authorization, memoization, auditing and metric/exception reporting. If you’re not familiar with AOP and its motivation, these articles might be a good start.

Frameworks like Spring and AspectJ in Java-land made AOP somewhat popular. Much of the criticism to AOP is directed towards the seemingly magic ways that code gets injected or substituted at runtime. It is quite difficult to trace a method call when aspects are involved by just looking at the code. Heavyweight tools like Eclipse or IntellijIDEA provide facilities for dealing with that.

An Aspect is the composition of a join-point and an advice. The join-point is a definition of when some advice can be applied. The most obvious example of a join-point is a simple method call. An advice is the definition of what code will be executed when the advice is applied. An Aspect is therefore the application of advices over join-points.

Motivation

Suppose that Expensive#computation is a method that takes a long time to complete and might get called many times. In order not to clutter the implementation of #computation with memoization details, you could solve the problem by subclassing Expensive in Cheap:

class Expensive
  def computation
    # ... a pretty complex method
  end

  # in a pretty complex class
end

class Cheap < Expensive
  # A pretty simple class
  def computation
    @__computation ||= super
  end
end

That solves the problem. But these problems does not occur in a single class. Your application could have a ton of those expensive methods and you don’t want to keep repeating the same memoization logic over and over again.

It’s a long time since I first heard of Ruby’s Module#prepend. At that time, the feature seemed weird and quite useless to me. Recently I’ve come across a great usage of Module#prepend for emulating AOP-ish behavior without resorting to method renaming/aliasing. ^2

With Module#prepend, you could “invert” the inheritance (see the class.ancestors discussion below to understand what I mean by that) and solve the problem like this:

module Memoize
  def computation
    @__computation ||= super
  end
end

class Expensive
  prepend Memoize

  def computation
    # ... a pretty complex method
  end

  # in a pretty complex class
end

The Memoize module could be prepended in an arbitrary number of classes and avoid the repetition of memoization logic all over the application.

Understanding Module#prepend

Module#prepend allows you to put the module’s methods in a higher priority than the methods defined in the class itself. Consider the following example:

module A
  def foo; :bar; end
end

class B
  def foo; :foo; end
end

B.new.foo # => :foo

class B
  prepend A
end

B.new.foo # => :bar

As you can see, after prepending module A in class B, B.new.foo will dispatch to A#foo instead of B#foo.

More interestingly, you could use super in A#foo, and that would delegate to B#foo:

module A
  def foo
    "foo and also #{super}"
  end
end

class B
  prepend A

  def foo
    :bar
  end
end

B.new.foo # => "foo and also bar"

That happens because Module#prepend will add the prepended module before the class itself itsin its ancestor chain (it will prepend into the ancestor list, hence the name):

B.ancestors # => [A, B, Object, Kernel, BasicObject]

With the previous explanation in mind, it would take you no time to figure out how to implement an around-advice using Module#prepend:

module A
  def foo
    puts 'stuff can be executed before original implementation'
    super
    puts 'and also after'

    puts 'Hence: "Around" advice'
  end
end

B.new.foo
# => "stuff can be executed before original implementation"
# => "foo and also bar"
# => "and also after"

On the road to meta-programming

The astute reader surely have noticed one short-coming in our prepended modules: When you invoke super, you call the next method with same name found in the ancestor chain. That is, we have the concepts of advice and join-point coupled, which definitely hinders the composability of advices.

In order to achieve the same functionality provided by mature AOP frameworks, we need to separate our implementations join-point and the advice. To do that, we will need to generate the prepended module (A in our previous example) on the fly.

In the next post of this series I will show how to achieve this level of dynamism and write completely non-intrusive (yet discoverable) advices for logging, metric reporting and so on.

That’s it.

(1) More references on Module#prepend can be found here and here.

(2) AOP-like behavior using method-aliasing can be seen in the after_do gem and in Rail’s old alias_method_chain. I have authored an extension gem called after_do-loader which applies after and before advices using a magic .yml file. That was then. Today I highly recommend you to take a Module#prepend based approach.

Comments