Ruby delegate.rb secrets

by Jim

You’ve seen SimpleDelegator in action and used it a bit yourself. The delegate library is more than just a fancy method_missing wrapper.

Easy Wrappers

First and foremost, SimpleDelegator is a fancy method_missing wrapper. I know I said the library was more than that, just bear with me.

Here’s some sample code:

jim = Person.new # some object

class Displayer < SimpleDelegator
  def name_with_location
    "#{__getobj__.name} of #{__getobj__.city}"
  end
end

displayer = Displayer.new(jim)

puts displayer.name_with_location #=> "Jim of Some City"

That Displayer class initializes with an object and automatically sets it as @delegate_sd_obj. You’ll also get both a __getobj__ and a __setobj__ method to handle the assignment of the @delegate_sd_obj.

You may want to alias those methods so they won’t be so ugly when you use them: alias_method :object, :__getobj__.

Method Missing

Here’s an expanded view of how it handles method_missing:

target = self.__getobj__ # Get the target object
if target.respond_to?(the_missing_method)
  target.__send__(the_missing_method, *arguments, &block)
else
  super
end

The actual code is a bit more compact than that, but it’s that simple. SimpleDelegator is so simple, in fact, that you can create your own implementation just like this:

class MyWrapper
  def initialize(target)
    @target = target
  end
  attr_reader :target

  def method_missing(method_name, *args, &block)
    target.respond_to?(method_name) ? target.__send__(method_name, *args, &block) : super
  end
end

That’s not everything, but if all you need is simple use of method_missing, this is how it works.

SimpleDelegator Methods

SimpleDelegator adds some convenient ways to see what methods are available. For example, if we have our jim object wrapped by displayer, what can we do with it? Well if we call displayer.methods we’ll get back a unique collection of both the object’s and wrapper’s methods.

Here’s what it does:

def methods(all=true)
  __getobj__.methods(all) | super
end

It defines the methods method and uses the union method “|” from Array to make a unique collection. The object’s methods are combined with those of the wrapper.

['a','b'] | ['c','b'] #=> ['a','b','c']

The same behavior is implemented for public_methods and protected_methods but not private_methods. Private methods are private, so you probably shouldn’t be accessing those from the outside anyway.

Why does it do this? Don’t we want to know that the main object and the SimpleDelegator object have methods of the same name?

Not really.

From the outside all we care to know is what messages we can send to an object. If both your main object and your wrapper have methods of the same name, the wrapper will intercept the message and handle it. What you choose to do inside your wrapper is up to you, but all these methods lists need to provide is that the wrapper can receive any of those messages.

Handling clone and dup

SimpleDelegator will also prepare clones and dups for your target object.

def initialize_clone(obj) # :nodoc:
  self.__setobj__(obj.__getobj__.clone)
end
def initialize_dup(obj) # :nodoc:
  self.__setobj__(obj.__getobj__.dup)
end

Read Jon Leighton’s post about initialize_clone, initialize_dup and initialize_copy in Ruby for more details about when those methods are called.

Making your own SimpleDelegator

SimpleDelegator actually inherits almost all of this from Delegator. In fact, the only changes that SimpleDelegator makes is 2 convenience methods.

class SimpleDelegator < Delegator
  def __getobj__
    @delegate_sd_obj
  end
  def __setobj__(obj)
    raise ArgumentError, "cannot delegate to self" if self.equal?(obj)
    @delegate_sd_obj = obj
  end
end

Subtracting all the comments around what those methods mean, that’s the entirety of the class definition as it is in the standard library. If you prefer to use your own and call it SuperFantasticDelegator, you only need to make these same getter and setter methods and you’ve got all that you need to replace SimpleDelegator.

Keep in mind, however, that the __setobj__ method has some protection in there against setting the target object to the wrapper itself. You’ll need to do that too unless you want to get stuck in an endless method_missing loop.

Using DelegateClass

The delegate library also provides a method called DelegateClass which returns a new class.

Here’s how you might use it:

class Tempfile < DelegateClass(File)
  def initialize(basename, tmpdir=Dir::tmpdir)
    @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600)
    super(@tmpfile)
  end

  # more methods here...
end

This creates a Tempfile class that has all the methods defined on File but it automatically sets up the message forwarding with method_missing.

Inside the DelegateClass method it creates a new class with klass = Class.new(Delegator).

Then it gathers a collection of methods to define on this new class.

methods = superclass.instance_methods
methods -= ::Delegator.public_api
methods -= [:to_s,:inspect,:=~,:!~,:===]

It gets the instance_methods from the superclass and subtracts and methods already in the Delegator.public_api (which is just the public_instance_methods). Then it removes some special string and comparison methods (probably because you’ll want to control these yourself and not have any surprises).

Next it opens up the klass that it created and defines all the leftover methods.

klass.module_eval do
  def __getobj__  # :nodoc:
    @delegate_dc_obj
  end
  def __setobj__(obj)  # :nodoc:
    raise ArgumentError, "cannot delegate to self" if self.equal?(obj)
    @delegate_dc_obj = obj
  end
  methods.each do |method|
    define_method(method, Delegator.delegating_block(method))
  end
end

The code is sure to define the __getobj__ and __setobj__ methods so that it will behave like SimpleDelegator. Remember, it’s copying methods from Delegator which doesn’t define __getobj__ or __setobj__.

What’s interesting here is that it’s using Delegator.delegating_block(method) to create each of the methods. That delegating_block returns a lambda that is used as the block for the method definition. As it defines each of those methods in the methods collection, it creates a forwarding call to the target object. Here’s the equivalent of what each of those methods will do:

target = self.__getobj__
target.__send__(method_name, *arguments, &block)

For every method that it gathers to define on this new DelegateClass it forwards the message to the target object as defined by __getobj__. Pay close attention to that. Remember that I pointed out how you can make your own SimpleDelegator and create your own getter and setter methods? Well DelegateClass creates methods that expect __getobj__ specifically. So if you want to use DelegateClass but don’t want to use that method explicitly, you’ll need to rely on alias_method to name it something else. All of your automatically defined methods rely on __getobj__.

Lastly, before returning the klass, the public_instance_methods and protected_instance_methods are defined. There’s some interesting things going on in those method definitions, but I’ll keep the explanation simple for now.

This Tempfile class that we created is actually exactly how the standard library’s Tempfile is defined.

If you’re not familiar with it, you can use it like this:

require 'tempfile'
file = Tempfile.new('foo')
# then do whatever you need with a tempfile

If you dive into that library you’ll see:

class Tempfile < DelegateClass(File)

The tempfile library relies on the delegate library, but not in the way that you might find in the wild. Often I see developers using only the SimpleDelegator class, but as you can see there’s a handful of other ways to make use of delegate to handle message forwarding for you.

Clean Up Your Code

If you liked this post and want more like it, get periodic tips in your inbox by leaving your email address below.

To get more information about cleaning up your objects, classes, and code, then check out Clean Ruby, an ebook which will describe ways to keep your code clean, maintainable, and focused on business value. Make your OOP more obvious, easier to understand, easier to test, and easier to maintain.



Find more in the archives

1999 - 2014 © Saturn Flyer LLC 2321 S. Buchanan St. Arlington, VA 22206

Call Jim Gay at 571 403 0338