Multiple ways in implementing delegation pattern in ruby

ruby patterns

Thu Dec 02 14:02:07 -0800 2010

There are multiple ways to implement the delegation pattern in ruby. This blog post will illustrate four of them. Let's provide some context first.

Harping on academia domain context (again), let's say a class Document has an attribute called @resource, which is an instance of some other class. For the sake of this instruction, let @resource be an instance of Article class. An instance of the Article class has the following attributes: @title [String], @content [String], @authors [Array]. @resource can also be an instance of Book class, Web resource class etc, which is why Article is a separate class added into Document. Title, content and authors can be considered the lowest common denominator attributes in @resource. So, if I want the title, content or authors of a document instance, implementing a delegation pattern will make more sense to keep the code tidier.

The code to support this context:

class Document
  # @todo implement delegation pattern
  attr_accessor :resource
  def initialize 
    @resource = nil
    yield self if block_given?    
  end
end

class Article
  attr_accessor :title, :content, :authors
  def initialize
    @title, @content, @authors = nil, nil, []
    yield self if block_given?
  end
end

# setting up the document
doc = Document.new do |doc|
  doc.resource = Article.new do |article|
    article.title = "Ze Title"
    article.content = "Ze Content"
    article.authors.unshift "Author 1", "Author 2"
  end
end

# The following statements below will yield undefined method errors!! 
# But upon implementing the delegation code, it should work....

# getters
puts doc.title              #=> print out "Ze Title"
puts doc.content            #=> print out "Ze Content"
puts doc.authors.join(", ") #=> print out "Author 1, Author 2 "

# setters
doc.title   = "New Title"     #=> @resource.title then: "New Title"
doc.content = "New Content"   #=> @resource.content then: "New Content"
doc.authors << "Author 3"     #=> @resource.authors then: ["Author 1", "Author 2", "Author 3"]

Implementing Delegation...

The updated Document class to support the title, content and authors instance methods will look as follows:

class Document

  attr_accessor :resource
  def initialize
    @resource = nil
    yield self if block_given?
  end

  # auto-generate the getter and setter methods of the resource attributes
  # 
  # @example
  #      def title
  #        @resource.title
  #      end
  #      def title=(title)
  #        @resource.title = title
  #      end
  #      .... and so on for the other attributes ....
  #
  def self.resource_attributes(*resource_attributes)
    resource_attributes.each do |resource_attribute|
      # getter
      define_method resource_attribute do
        @resource.send resource_attribute
      end
      # setter
      define_method "#{resource_attribute.to_s}=" do |value| 
        @resource.send "#{resource_attribute.to_s}=", value
      end
    end
  end
  resource_attributes :title, :content, :authors  
end

The updated Document class to support the title, content and authors instance methods will look as follows:

class Document

  attr_accessor :resource
  def initialize 
    @resource = nil
    yield self if block_given?    
  end

  # Pass along the method to @resource object if it responds to the method 
  # the Document instance doesn't have defined. NoMethodError if nothing else
  def method_missing(method, *args, &block)    
    if @resource.respond_to?(method)
      @resource.send(method, *args, &block)
    else
      raise NoMethodError  
    end    
  end

end

Pretty cleaned up no? Essentially, if the @resource instance responds to the title, content and/or authors method, these respective methods will be invoked by the @resource object. If not, just raise the error that was going to be raised by default anyhow.

Using the core Forwardable module, these delegations can be cleaned up even more.

require 'forwardable'

class Document
  
  extend Forwardable
  
  attr_accessor :resource
  
  def_delegators :@resource, :title, :content, :authors
  
  def initialize 
    @resource = nil
    yield self if block_given?    
  end
  
end

def_delegators does the magic of delegating the title, content and authors methods to @resource.

Active Support Delegate accomplishes the delegation pattern like Forwardable but reads better. It's using the gut of the Method Missing approach. Some people might prefer this. Furthermore, there are are some additional options added to this like 'prefix' and 'allow_nil'.

require 'rubygems'
require 'active_support'

class Document

  attr_accessor :resource
  def initialize 
    @resource = nil
    yield self if block_given?    
  end

  delegate :title, :content, :authors, :to => :@resource
    
end

There. Now go delegate to minions my minions! Cheers.

blog comments powered by Disqus