Delegating Method Calls to Another Object

Problem

You'd like to delegate some of an object's method calls to a different object, or make one object capable of " impersonating" another.

Solution

If you want to completely impersonate another object, or delegate most of one object's calls to another, use the delegate library. It generates custom classes whose instances can impersonate objects of any other class. These custom classes respond to all methods of the class they shadow, but they don't do any work of their own apart from calling the same method on some instance of the "real" class.

Here's some code that uses delegate to generate CardinalNumber, a class that acts almost like a Fixnum. CardinalNumber defines the same methods as Fixnum does, and it takes a genuine Fixnum as an argument to its constructor. It stores this object as a member, and when you call any of Fixnum's methods on a CardinalNumber object, it delegates that method call to the stored Fixnum. The only major exception is the to_s method, which I've decided to override.

	require 'delegate'

	# An integer represented as an ordinal number (1st, 2nd, 3rd…), as
	# opposed to an ordinal number (1, 2, 3…) Generated by the
	# DelegateClass to have all the methods of the Fixnum class.
	class OrdinalNumber < DelegateClass(Fixnum)
	 def to_s
	 delegate_s = __getobj_ _.to_s
	 check = abs
	 if to_check == 11 or to_check == 12
	 suffix = "th"
	 else
	 case check % 10
	 when 1 then suffix = "st"
	 when 2 then suffix = "nd"
	 else suffix = "th"
	 end
	 end
	 return delegate_s + suffix
	 end
	end

	4.to_s # => "4"
	OrdinalNumber.new(4).to_s # => "4th"

	OrdinalNumber.new(102).to_s # => "102nd"
	OrdinalNumber.new(11).to_s # => "11th"
	OrdinalNumber.new(-21).to_s # => "-21st"

	OrdinalNumber.new(5).succ # => 6
	OrdinalNumber.new(5) + 6 # => 11
	OrdinalNumber.new(5) + OrdinalNumber.new(6) # => 11

 

Discussion

The delegate library is useful when you want to extend the behavior of objects you don't have much control over. Usually these are objects you're not in charge of instantiatingthey're instantiated by factory methods, or by Ruby itself. With delegate, you can create a class that wraps an already existing object of another class and modifies its behavior. You can do all of this without changing the original class. This is especially useful if the original class has been frozen.

There are a few methods that delegate won't delegate: most of the ones in Kernel. public_instance_methods. The most important one is is_a?. Code that explicitly checks the type of your object will be able to see that it's not a real instance of the object it's impersonating. Using is_a? instead of respond_to? is often bad Ruby practice, but it happens pretty often, so you should be aware of it.

The Forwardable module is a little more precise and a little less discerning: it lets you delegate any of an object's methods to another object. A class that extends Forwardable can use the def_delegator decorator method, which takes as arguments an object symbol and a method symbol. It defines a new method that delegates to the method of the same name in the given object. There's also a def_delegators method, which takes multiple method symbols as arguments and defines a delegator method for each one. By calling def_delegator multiple times, you can have a single Forwardable delegate different methods to different subobjects.

Here I'll use Forwardable to define a simple class that works like an array, but supports none of Array's methods except the append operator, <<. Note how the << method defined by def_delegator is passed through to modify the underlying array.

	class AppendOnlyArray
	 extend Forwardable
	 def initialize
	 @array = []
	 end

	 def_delegator :@array, :<<
	end

	a = AppendOnlyArray
	a << 4
	a << 5
	a.size
	# => undefined method 'size' for #

AppendOnlyArray is pretty useless, but the same principle makes Forwardable useful if you want to expose only a portion of a class' interface. For instance, suppose you want to create a data structure that works like a Hash, but only supports random access. You don't want to support keys, each, or any of the other ways of getting information out of a hash without providing a key.

You could subclass Hash, then redefine or delete all the methods that you don't want to support. Then you could worry a lot about having missed some of those methods. Or you could define a subclass of Forwardable and define only the methods of Hash that you do want to support.

	class RandomAccessHash
	extend Forwardable
	 def initialize
	 @delegate_to = {}
	 end
	
	 def_delegators :@delegate_to, :[], "[]="
	end

	balances_by_account_number = RandomAccessHash.new

	# Load balances from a database or something.
	balances_by_account_number["101240A"] = 412.60
	balances_by_account_number["104918J"] = 10339.94
	balances_by_account_number["108826N"] = 293.01

Random access works if you know the key, but anything else is forbidden:

	balances_by_account_number["104918J"] # => 10339.94
	balances_by_account_number.each do |number, balance|
	 puts "I now know the balance for account #{number}: it's #{balance}"
	end
	# => 
NoMethodError: undefined method 'each' for #

 

See Also

  • An alternative to using SimpleDelegator to write delegator methods is to skip out on the methods altogether, and instead implement a method_missing which does the delegating. Recipe 2.13, "Simulating a Subclass of Fixnum," uses this technique. You might especially find this recipe interesting if you'd like to make arithmetic on CardinalNumber objects yield new CardinalNumber objects instead of Fixnum objects.


Strings

Numbers

Date and Time

Arrays

Hashes

Files and Directories

Code Blocks and Iteration

Objects and Classes8

Modules and Namespaces

Reflection and Metaprogramming

XML and HTML

Graphics and Other File Formats

Databases and Persistence

Internet Services

Web Development Ruby on Rails

Web Services and Distributed Programming

Testing, Debugging, Optimizing, and Documenting

Packaging and Distributing Software

Automating Tasks with Rake

Multitasking and Multithreading

User Interface

Extending Ruby with Other Languages

System Administration



Ruby Cookbook
Ruby Cookbook (Cookbooks (OReilly))
ISBN: 0596523696
EAN: 2147483647
Year: N/A
Pages: 399

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net