Friday, June 09, 2006

The State Pattern, Ruby-style

One of the cool things about using a language that allows for metaprogramming is that you can reduce the amount of boilerplate code you have to write. Common patterns can be put into libraries, and give you a small DSL that you can use to replicate this pattern all over your code. A good example of a common pattern with lots of boilerplate is the state pattern: where the behavior of an object changes depending on the internal state of the object. This can be accomplished by using a simple 'if' statement, or by modeling a finite state machine.

Check out a sample implementation on the RubyGarden wiki. We have a class for each state, a common parent class for the states, and a context class that delegates to the current active state class. That's a lot of code that doesnt really do much. Enter the StatePattern module, which puts all of this boilerplate in a small mix-in module, and ties everything together with some metaprogramming glue. Now we can write:

class Connection
include StatePattern
state :initial do
def connect
puts "connected"
transition_to :connected, "hello from initial state"
end
def disconnect
puts "not connected yet"
end
end
state :connected do
def initialize(msg)
puts "initialize got msg: #{msg}"
end
def connect
puts "already connected"
end
def disconnect
puts "disconnecting"
transition_to :initial
end
end
def reset
puts "reseting outside a state"
transition_to :initial
end
end

c = Connection.new
c.disconnect # not connected yet
c.connect # connected
# initialize got msg: hello from initial state
c.connect # already connected
c.disconnect # disconnecting
c.connect # connected
# initialize got msg: hello from initial state
c.reset # reseting outside a state
c.disconnect # not connected yet

.. and the boilerplate is all gone. How's it all work? Each call to state defines a new subclass of Connection that is stored in a hash. Then, a call to transition_to instantiates one of these subclasses and sets it to the be the active state. Method calls to Connection are delegated to the active state object via method_missing. This was a tricky one to get right! Download the source from here.

2 Comments:

At 9:55 PM, Blogger MonkeeSage said...

Here's a different take on a State-ful module:

module Stateful
    def accept_states(*states)
      @states = {}
      states.each do |s|
        @states[s] = nil
      end
    end
    def transition_to(s)
      if (@states.has_key?(s))
        if (@states[s])
          @states[s].call
        end
      end
    end
    def state(s, &b)
      if (@states.has_key?(s))
        if (block_given?)
          @states[s] = b
        end
      end
    end
end

########

class Connection
    include Stateful
    def initialize
      accept_states(:initial, :connected)
      state(:initial) do
        def connect
          puts("connected")
          transition_to(:connected)
        end
        def disconnect
          puts("not connected yet")
        end
      end
      state(:connected) do
        def connect
          puts("already connected")
        end
        def disconnect
          puts("disconnecting")
          transition_to(:initial)
        end
      end
      transition_to(:initial)
    end
    def reset()
      puts("reseting outside a state")
      transition_to(:initial)
    end
end

c = Connection.new
c.disconnect # not connected yet
c.connect # connected
c.connect # already connected
c.disconnect # disconnecting
c.connect # connected
c.reset # reseting outside a state
c.disconnect # not connected yet

 
At 8:24 PM, Blogger mats said...

nice implementation. exactly what I need.

 

Post a Comment

<< Home