Saturday, June 24, 2006

Ruby, the Consummate Delegator

Maybe one of the reasons inheritance is so overused in certain static languages like Java and C# is that it can be a pain in the ass to write those tiny methods that hand off operations to the delegate class.

Ruby, on the other hand, makes it a no-brainer with the method_missing callback.

Here a ShuffleArray extends Array to add some shuffling behavior. Since a ShuffleArray's relationship to Array is an "is a" relationship, that seems a valid use of inheritance.

But a CardDeck is much more than a ShuffleArray - or will be anyway as it will provide more behavior in the future - and its relationship to ShuffleArray is more of a "uses a" association. Still I'd like CardDeck to be able to make use of all the nifty methods and properties of a ShuffleArray, and I'd like to do that without having to write all those methods to pass through from CardDeck to ShuffleArray.

To do that CardDeck uses Ruby's method_missing callback to delegate any messages other than its own to the ShuffleArray that it composes. This way CardDeck gets the same behaviors and properties of a ShuffleArray (and Array by extension) without having to subclass ShuffleArray and without having to explicitly define those pass-through delegate methods.

Each class is very small, cohesive, and extensible.

class ShuffleArray < Array
def shuffle
sort_by { rand }
end
end

class CardDeck
alias :method_missing_orig :method_missing
def initialize(cards)
@cards = ShuffleArray.new(cards)
end
def method_missing(m, *args, &block)
if @cards.respond_to?(m)
@cards.send(m, *args, &block)
else
method_missing_orig(m, *args, &block)
# or you could just raise the exception or return nil
# raise NoMethodError
end
end
end

class Card
attr_reader :face, :suit
def initialize(face, suit)
@face, @suit = face, suit
end
def to_s
puts "#{face} of #{suit}"
end
end

aos = Card.new("Ace", "Spades")
kod = Card.new("King", "Diamonds")
qoh = Card.new("Queen", "Hearts")
joc = Card.new("Jack", "Clubs")

cards = [aos, kod, qoh, joc]
card_deck = CardDeck.new(cards)

irb(main):041:0> puts card_deck.size
4
=> nil
irb(main):042:0> card_deck.each {|card| puts card }
Ace of Spades
#<Card:0x2c1d510>
King of Diamonds
#<Card:0x2c19820>
Queen of Hearts
#<Card:0x2c15c08>
Jack of Clubs
#<Card:0x2c12170>
=> [#<Card:0x2c1d510 @suit="Spades", @face="Ace">,
#<Card:0x2c19820 @suit="Diamonds", @face="King">,
#<Card:0x2c15c08 @suit="Hearts", @face="Queen">,
#<Card:0x2c12170 @suit="Clubs", @face="Jack">]

irb(main):043:0> puts card_deck[3]
Jack of Clubs
#<Card:0x2c12170>
=> nil

irb(main):044:0> cards_shuffled = card_deck.shuffle
=> [#<Card:0x2c15c08 @suit="Hearts", @face="Queen">,
#<Card:0x2c1d510 @suit="Spades", @face="Ace">,
#<Card:0x2c19820 @suit="Diamonds", @face="King">,
#<Card:0x2c12170 @suit="Clubs", @face="Jack">]

irb(main):046:0> cards_shuffled = card_deck.shuffle
=> [#<Card:0x2c12170 @suit="Clubs", @face="Jack">,
#<Card:0x2c15c08 @suit="Hearts", @face="Queen">,
#<Card:0x2c1d510 @suit="Spades", @face="Ace">,
#<Card:0x2c19820 @suit="Diamonds", @face="King">]

irb(main):050:0> cards_shuffled = card_deck.shuffle
=> [#<Card:0x2c1d510 @suit="Spades", @face="Ace">,
#<Card:0x2c12170 @suit="Clubs", @face="Jack">,
#<Card:0x2c19820 @suit="Diamonds", @face="King">,
#<Card:0x2c5c08 @suit="Hearts", @face="Queen">]

irb(main):051:0> card_deck.find {|c| c.face == "Ace" }
=> #<Card:0x2c1d510 @suit="Spades", @face="Ace">

irb(main):052:0> card_deck.reverse
=> [#<Card:0x2c12170 @suit="Clubs", @face="Jack">,
#<Card:0x2c15c08 @suit="Hearts", @face="Queen">,
#<Card:0x2c19820 @suit="Diamonds", @face="King">,
#<Card:0x2c1d510 @suit="Spades", @face="Ace">]

irb(main):053:0> card_deck.sort_by { |c| c.suit }
=> [#<Card:0x2c12170 @suit="Clubs", @face="Jack">,
#<Card:0x2c19820 @suit="Diamonds", @face="King">,
#<Card:0x2c15c08 @suit="Hearts", @face="Queen">,
#<Card:0x2c1d510 @suit="Spades", @face="Ace">]

Other approaches might include adding the shuffle behavior to the core Array class itself, but changing core class behavior might get you in trouble with other core or imported classes.

UPDATE: Thanks to Roy and Rick for getting me to think about this a bit more. Earlier I was needlessly delegating shuffle behavior from ShuffleArray to Array with the overhead of an extra call to method_missing.

6 Comments:

Blogger Roy said...

One other way to go is to add the shuffle method directly to class Array, e.g.,

class Array
def Shuffle
sort_by {rand}
end
end

6:15 PM  
Anonymous Rick Gordon said...

the delegate approach is used by other languages supporting dynamic binding as well.
I have to admit I like Roy's solution better: in this case a simple class extension would seem to be the way to go.
Besides being syntactically simpler, it also doesn't incur a "method_missing" + send call each time you use it.

I see both of these forms used in Cocoa programming a lot too. Delegation is a standard model, although there the sender looks to see if the delegate provides the method before calling it. Probably a little faster than the method-missing exception incurred there. Extensions are quite popular as well - although they first annoyed me, with the assistance of the IDE I can now quickly find out where a method is *really* implemented.

9:20 AM  
Blogger victorcosby said...

Roy and Rick:
I definitely see your points about the ShuffleArray. I've probably gone overboard on delegation for that, though I think I'd favor subclassing Array to build ShuffleArray instead of opening Array since it's a core class and I might only need that behavior in one place. It would probably depend upon the context of where the code might run. If I'm building a library to play nicely alongside other systems, or my code has to live inside something like Rails, I'd probably be more conservative about meddling with too many core classes.

And while a ShuffleArray could really be better thought of as an "is a" relationship in terms of Array, I think the CardDeck is more of a "uses a" relationship in terms of ShuffleArray, so I'd probably still want to use delegation there, especially as I would intend to provide more behavior for that class while wanting it to easily make use of all the methods available to ShuffleArray and Array by extension.

Thanks for getting me to think about this a bit more. I'll update the post with some of these changes.

9:42 AM  
Blogger victorcosby said...

Rick:
Ruby also has way of checking whether an object responds to a method with respond_to?. I've updated the CardDeck example to show how you would verify the send before passing it to the delegate and raise an exception if it doesn't.

As for the amount of overhead incurred with method_missing, that's a good question worth exploring.

Rails makes extensive use of method_missing. For example, ActiveRecord uses it to expose model attributes dynamically. And ActionController uses it for request routing. It will attempt to forward an undefined action to a template by the same name.

11:10 AM  
Blogger Roy said...

LOL--I wasn't going for anything profound w/my comment. I'm just so freaked out by ruby's allowing you to directly add to standard classes that I look for places to try it. ;-)

If I'm not mistaken, you can even add methods to individual *instances* of a given class:

-----------------
irb(main):001:0> my_array = %w(one two three four)
=> ["one", "two", "three", "four"]
irb(main):002:0> def my_array.shuffle
irb(main):003:1> sort {rand}
irb(main):004:1> end
=> nil
irb(main):005:0> my_array.class
=> Array
irb(main):006:0> my_array.shuffle
=> ["four", "three", "one", "two"]
irb(main):007:0> another_array = %w(raz dva tri chtiri)
=> ["raz", "dva", "tri", "chtiri"]
irb(main):008:0> another_array.shuffle
NoMethodError: undefined method `shuffle' for ["raz", "dva", "tri", "chtiri"]:Array from (irb):8
irb(main):009:0>
-----------------

So very freaky.

Happy rubying...

-Roy

2:12 PM  
Anonymous kelly said...

Roy,
Small point -- you have a puts in your to_s method. I think to_s should return a string, and puts would return nil.

6:34 PM  

Post a Comment

Links to this post:

Create a Link

<< Home