Thursday, August 23, 2007

Ruby: State pattern using Modules and Facets

While browsing the docs for Facets I noticed the Kernel.as method. Then, a day later I needed a solution that added behavior using the state pattern; however, it was more elegant for our solution if the state instances could call methods on the class containing the state object. Our first solution was to delegate to the containing class via the method_missing method of the state instances. While this did work, debugging became problematic when things went wrong.

I did a quick spike to see if Kernel.as could provide a solution.

require 'rubygems'
require 'facets'

module James
protected

def name
"James #{last_name}"
end
end

module Lynn
protected

def name
"Lynn #{last_name}"
end
end

class FamilyMember
attr_accessor :person
include James
include Lynn

def name
as(person).name
end

def last_name
"Holbrook"
end
end


member = FamilyMember.new
member.person = James
member.name # => "James Holbrook"
member.person = Lynn
member.name # => "Lynn Holbrook"
member.first_name rescue "first_name is not defined" # => "first_name is not defined"

There's a few interesting things about this solution. I can change the behavior at runtime by changing the person attribute. Also, the behavior is actually on the class instead of living on an independent state object that delegates back to the class. Lastly, each method is protected so they must be accessed via the delegations, and an error will occur if they are accessed directly.

4 comments:

  1. Anonymous4:33 PM

    "each method is protected so they must be accessed via the delegations, and an error will occur if they are accessed directly"

    You got me here. Till now I used to think that Ruby copies all the definition of a module into the definition of the class that includes it. Going by that logic, I couldn't explain your program's behavior as name method would be redefined and the modules' methods would be lost... I consulted "Programming Ruby":

    If a module is included within a class definition, the module's constants, class variables, and instance methods are effectively bundled into an anonymous (and inaccessible) superclass for that class. In particular, objects of the class will respond to messages sent to the module's instance methods.

    Your program made better sense now. The original definitions were not lost as they reside in a superclass (anonymous one). However this brought an interesting question -- what happens when two modules are included in a class... surely one class cannot have two superclasses! I wrote some code that proved there was only one superclass and any included module overwrote any methods that were included already by an earlier included module.

    I'll have to check what Kernel#as does 'cos I still don't get it how you have not lost the definitions of any name() methods... ie, unless you explain the same here to save me all that hunting. ;-)

    ReplyDelete
  2. Anonymous11:37 AM

    Hello Aman,

    As always, thanks for the comment.

    Here's an explanation: http://blog.jayfields.com/2007/08/ruby-calling-methods-of-specific.html

    ReplyDelete
  3. Anonymous12:05 PM

    As always, thanks for the explanation, Jay. :-)

    Hmm... I really should have checked the class's ancestors. A class cannot have multiple inheritence but it sure can have multi-level inheritence. That approach pretty much explains it all. :-)

    Thanks for bringing it to my notice and also explaining the "As" stuff. Really cool.

    ReplyDelete
  4. For anyone interested, here's another state pattern implementation http://github.com/dcadenas/state_pattern

    ReplyDelete

Note: Only a member of this blog may post a comment.