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.