Invisible proxies with Ruby
One of the coolest aspects of ActiveRecord is its support for associations. You use a domain-specific language to define your associations, and collection methods are defined so that you can access them.
Here's an example:
class Student < ActiveRecord::Base
has_many :exams
end
Student.find(:first).exams # => [#<Exam: @title => 'Maths', ...]
Student.find(:first).exams.class # => Array
From this example, has_many
seems fairly simple to implement. Just add an instance variable called exams for each Student, then add accessor methods.
However, we can also add new records to the database like this:
Student.find(:first).exams << Exam.new('Maths')
But how does that work? Student#exams
returns an Array, so how can you add records to the database using Array#<<
?
The way ActiveRecord does this is using an invisible proxy. In fact, Student#exams
doesn't return an Array
object, it returns a HasManyAssociation
object which dresses up as an Array
on the weekends.
Implementing an Invisible Proxy
There are several steps to creating a proxy class. If all this story-telling is getting a bit much, jump ahead and read all the code. Otherwise, read on!
Clean the Slate!
The first step is to undefine all the instance methods your proxy class has, except those which are definitely needed:
class InvisibleProxy
instance_methods.each do |m|
undef_method(m) unless m =~ /(^__|^nil\?$|^send$|^object_id$)/
end
end
This stops your proxy responding to all methods except the essentials: __id__
, __send__
, nil?
, send
, and object_id
. Note that this includes methods like class
and kind_of?
, so the class hierarchy is being subverted: your proxy will masquerade as its target.
Forward!
Then, you need your proxy implementation. We just forward all calls to respond_to?
and any missing methods on to the target object:
class InvisibleProxy
def initialize
@target = [1, 2, 3, 4]
end
def respond_to?(symbol, include_priv=false)
@target.respond_to?(symbol, include_priv)
end
private
def method_missing(method, *args, &block)
@target.send(method, *args, &block)
end
end
Override!
Now every instance of InvisibleProxy
appears to be an Array
with value [1, 2, 3, 4]
. You can add instance methods to your proxy class to override or add to those provided by Array
, and your users need never know.
For example, you might want Array#<<
to act like Array#+
when passed an array:
class InvisibleProxy
def <<(object)
if object.is_a? Array
@target += object
else
@target << object
end
end
end
Coda and Code
Invisible proxies are fun. Use them! Here's a gist with some sample code for you to play with.
Note: this post was originally written in December 2005. I dug it out and updated it a little bit for 2012.