Ruby Proxy Pattern and Dynamic Delegation with ActiveSupport BasicObject

This article targets Rails 2.3 Rails 3

The information contained in this page might not apply to different versions.

This is article is part of my series Understanding Ruby and Rails. Please see the table of contents for the series to view the list of all posts.

If you looked into the changes in Ruby 1.9, chances are that you already noticed Object is no longer the class at the top of the object-hierarchy in Ruby.

ruby-1.8.7-p249 > String.ancestors
# => [String, Enumerable, Comparable, Object, Kernel]
ruby-1.9.1-p378 > String.ancestors
# => [String, Comparable, Object, Kernel, BasicObject]

Prior 1.9, every Ruby object is a child of Object or, from an other point of view, Object is the parent of every object including Class and Modules.

ruby-1.8.7-p249 > String.ancestors
# => [String, Enumerable, Comparable, Object, Kernel]
ruby-1.8.7-p249 > String.class
# => Class
ruby-1.8.7-p249 > String.class.ancestors
# => [Class, Module, Object, Kernel]
ruby-1.8.7-p249 >

In Ruby 1.9 the grandpa role is played by a new special object called BasicObject. BasicObject can be considered a minimalist Object, an object with a very few defined methods.

ruby-1.9.1-p378 > BasicObject.instance_methods
 => [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__]

Proxy Pattern and Dynamic Delegation

For the most part of Ruby code, this change won't mean anything special. However the BasicObject plays a fundamental role to implement the proxy pattern and dynamic delegation.

Let me show you an example. Let's assume you need to log all method calls made to a bank Account object without changing the original Account definition. You can create a new class to wrap the Account object, log any method call then forward the request to the target object.

Here's a super simple bank Account.

class Account

  def initialize(amount)
    @amount = amount
  end

  def deposit(amount)
    @amount += amount
  end

  def withdraw(amount)
    @amount -= amount
  end

  def balance
    @amount
  end

  def inspect
    puts "Amount: #{@amount}"
  end

end

and here's our AccountLogger class.

class AccountLogger

  def initialize(amount)
    @operations = []

    @target = Account.new(amount)
    @operations << [:initialize, [amount]]
  end

  def method_missing(method, *args, &block)
    @operations << [method, args]
    @target.send(method, *args, &block)
  end

  def operations
    @operations
  end

end

The AccountLogger is our Proxy object. It doesn't know anything about the target object, in fact it limits itself to log method calls and forward the request to the underlying @target using the method_missing hook.

Let's run a few operations on a new bank Account.

account = AccountLogger.new(10)

account.operations
# => [[:initialize, [10]]]

account.balance
# => 10
account.operations
# => [[:initialize, [10]], [:balance, []]]

account.deposit(20)

account.balance
# => 30
account.operations
# => [[:initialize, [10]], [:balance, []], [:deposit, [20]], [:balance, []]]

So far, everything seems to work as expected. The AccountLogger is executing all the operations without complaining and it also logs every single operation in the internal stack.

But let's try something different. Let's print the human-readable representation of the Account.

account.inspect
# => Amount: 30
     "#"

account.operations
[[:initialize, [10]], [:balance, []], [:deposit, [20]], [:balance, []]]

Hey, it doesn't work! Not only it returns a messy response, it also forgot to log the call. The problem is that #inspect is actually defined in our AccountLogger.

#inspect is an instance method of the Object class and because every object inherits from Object, then our AccountLogger responds to #inspect. The method_missing is never triggered and our proxy miserably fails to work.

Here we have a serious method clashing problem. Because we can't change our Account implementation, we need to manually remove every method in the AccountLogger class which might conflict with the Account object.

class AccountLogger

  undef_method :inspect

  # ...

end

This is an error-prone approach, usually not applicable for libraries with a complex API.

Here comes the BasicObject. BasicObject actually prevents this issue by exposing a limited set of instance methods. Simply put, it copes with the problem from a different point of view: instead of taking a rich object and removing all the unnecessary methods, take a poor object and only define what you actually need.

At this point, we can change the AccountLogger to inherit from BasicObject.

class AccountLogger < BasicObject

  # ...

end

If you are looking for a real-world example, check out the Refactoring & Dynamic Delegator video from Railscasts.

What about BasicObject in Ruby 1.8?

There have been plenty of hacks and attempts to create a BasicObject in Ruby 1.8 with many different approaches: Sequel::BasicObject, HTTParty BasicObject and Builder::BlankState.

The latter is by far the most famous BlankObject implementation for Ruby 1.8. AFAIK it has been the real first attempt to introduce a basic object in the Ruby community and, if remember well, the Ruby 1.9 core implementation was largely inspired by the Builder library.

Recently, a proposal have been made to back-port BasicObject to Ruby 1.8.8. Here's Matz comment:

I see very little (virtually no) chance for the idea, that even enhance the gap between 1.8 and 1.9.

BasicObject and Rails

At this point, it's time to talk a bit about Rails. After all, this series is called understanding Ruby and Rails. As you might guess, ActiveSupport provides its own implementation of the BasicObject called ActiveSupport::BasicObject.

Actually, in Rails 2.3.5 it doesn't create anything new. It simply returns a modified instance of BasicObject or Builder::BlankSlate depending on whether the Ruby version is 1.9 or not.

The library has been modified in Rails 3 dropping the Builder dependency. That said, in Ruby 1.8.7 you can define a BasicObject-like object in 4 lines of code:

class BasicObject
  instance_methods.each do |m|
    undef_method(m) if m.to_s !~ /(?:^__|^nil?$|^send$|^object_id$)/
  end
end

As usual, if you need to inherit from a BasicObject in your Rails code, use ActiveSupport::BasicObject. In this way you don't have to reinvent the wheel again and you can safely rely on a cross-version implementation.