Getting to Know the Ruby Standard Library – Delegator
Today we will examine ruby’s implementation of the
Proxy pattern, with the
Delegator
class. We have already seen and example of it in use with
WeakRef
. A
Delegator
can be used when you want to intercept calls to some object without concerning the caller. For example we can use a
Delegator
to hide the latency from http calls or other slow code:
require 'delegate'
class Future < SimpleDelegator
def initialize(&block)
@_thread = Thread.start(&block)
end
def __getobj__
__setobj__(@_thread.value) if @_thread.alive?
super
end
end
The
Future
will invoke whatever is passed to it without blocking the rest of your code until you try to access the result. You could use it to issue several http requests and then process them later:
require 'net/http'
# These will each execute immediately
google = Future.new{ Net::HTTP.get_response(URI('http://www.google.com')).body }
yahoo = Future.new{ Net::HTTP.get_response(URI('http://www.yahoo.com')).body }
# Each of these blocks until its request has loaded
puts google
puts yahoo
In this example,
google
and
yahoo
will both spawn threads, however when we try to print them, the
Future
instance will block until the thread is done, and then pass on method calls to the result of our http call. You can
grab the code from github and give it try yourself. Lets take a look at how
Delegator
works. Open up the source, and follow along, if you have
Qwandry installed
qw delegate
will do the trick.
The file
delegate.rb
defines two classes,
Delegator
, an abstract class, and
SimpleDelegator
which implements the missing methods in
Delegator
. Let’s look at the first few lines of
Delegator
:
class Delegator
[:to_s,:inspect,:=~,:!~,:===].each do |m|
undef_method m
end
#...
You will notice that a block of code is being executed as part of the ruby class definition. This is entirely valid, and will be executed in the scope of the
Delegator
class.
undef_method
is called to remove the default implementations of some common methods defined by
Object
. We will see why in a little bit. Next up is the initializer:
def initialize(obj)
__setobj__(obj)
end
The method
__setobj__
might look strange, but it is just a normal method with an obscure name. When a
Delegator
is instantiated, the object it is delegating to is stored away with
__setobj__
. Next look at how
Delegator
implements
method_missing
, and you’ll see what all the prep work was for:
def method_missing(m, *args, &block)
#...
target = self.__getobj__
unless target.respond_to?(m)
super(m, *args, &block)
else
target.__send__(m, *args, &block)
end
#...
method_missing
is defined on
Object
, and will be called any time that you try to call method that is not defined. This is the key to how
Delegator
works, any methods not defined on
Delegator
are handled by this method. Before we dive into this, we should be aware of what the arguments to
method_missing
are. The
m
is the missing method’s name as a symbol. The
args
are zero or more arguments that would have been passed to that method. The
&block
is the block that the method was called with, or
nil
if no block was given.
The first thing
method_missing
does here is call
__getobj__
. We’ve already seen
__setobj__
, it sets the object that
Delegator
wraps, so we can reason that
__getobj__
gets it. Once the wrapped object has been obtained, it checks to see if that object implements the method we want to call. If not, then we call
Object#method_missing
, which is going to raise an exception. If the wrapped object does implement our method, then it passes it on. The methods that were undefined earlier are guaranteed to be passed on to the wrapped object, and the odd looking
__getobj__
and
__setobj__
are unlikely to collide with any other object’s methods. Ruby’s flexibility really shines in this example, in just a few lines of code, we get a very useful class that can be used to implement advanced behavior.
Now let’s figure out why there are two classes defined here. If you look down at
Delegator#__setobj__
and
Delegator#__getobj__
we’ll see something interesting:
def __getobj__
raise NotImplementedError, "need to define `__getobj__'"
end
def __setobj__(obj)
raise NotImplementedError, "need to define `__setobj__'"
end
Neither of these methods are implemented, effectively making
Delegator
an abstract class. For connivence,
SimpleDelegator
implements them in a reasonable manner. There are a few other special methods defined on
Delegator
as well:
def ==(obj)
return true if obj.equal?(self)
self.__getobj__ == obj
end
First
Delegator
checks for equality against itself, then it checks it against the wrapped object. This way
==
will return true if you pass in either the wrapped object, or the delegate itself. In this same manner you can intercept calls to specific methods, or override
Delegator#method_missing
to intercept all calls.
We have learned about a powerful design pattern that is easily implemented in ruby. We also saw a very good use for ruby’s
method_missing
. How have you used
Delegator
or found similar proxy patterns in ruby?
More articles in this series
- Getting to Know the Ruby Standard Library – Delegator
- Getting to Know the Ruby Standard Library – WeakRef
- Getting to Know the Ruby Standard Library – Timeout
- Getting to Know the Ruby Standard Library – Pathname
- Getting to Know the Ruby Standard Library – Abbrev
- Getting to Know the Ruby Standard Library – TSort
- Getting to Know the Ruby Standard Library – MiniTest::Mock
- Getting to Know the Ruby Standard Library – Shellwords
- Getting to Know the Ruby Standard Library – MiniTest