login

StatePattern (Ruby)

HomePage | RecentChanges | Preferences | Wikis | RubyGarden | Feed-icon-16x16

This page contains changes that are awaiting review.
"Allow an object to alter its behavior when its internal state changes. The object will appear to change its class." [1]

To allow the state object to change the state of the context without violating encapsulation, an interface to the outside world can be wrapped around a context object.

class Client
  def initialize
    @context = Context.new
  end
  def connect
    @context.state.connect
  end
  def disconnect
    @context.state.disconnect
  end
  def send_message(message)
    @context.state.send_message(message)
  end
  def receive_message
    @context.state.receive_message
  end
private
  class Context
    def initialize
      @state = Offline.new(self)
    end
    attr_accessor :state
  end
end

class ClientState
  def initialize(context)
    @context = context
    inform
  end
end

class Offline < ClientState
  def inform
    puts "offline"
  end
  def connect
    @context.state = Online.new(@context)
  end
  def disconnect
    puts "error: not connected"
  end
  def send_message(message)
    puts "error: not connected"
  end
  def receive_message
    puts "error: not connected"
  end
end

class Online < ClientState
  def inform
    puts "connected"
  end
  def connect
    puts "error: already connected"
  end
  def disconnect
    @context.state = Offline.new(@context)
  end
  def send_message(message)
    puts "\"#{message}\" sent"
  end
  def receive_message
    puts "message received"
  end
end

client = Client.new
client.send_message("Hello")
client.connect
client.send_message("Hello")
client.connect
client.receive_message
client.disconnect

Running the code above results in the output:

offline
error: not connected
connected
"Hello" sent
error: already connected
message received
offline

-- JasonArhart

Q: What is the purpose of the Context object ? Couldn't the Client hold a ClientState? object ?

A: Encapsulation. The Context object is the real object. The Client object is just a wrapper that protects the state from being changed directly.

[I believe this does not answer the question. Encapsulation is kept if the object of class Client holds a reference to a ClientState? object:

 class Client
   def initialize
     @state = Offline.new
   end

   def method_missing(meth, *args, &block)
     @state = @state.send(meth, *args, &block)
     nil # dummy
   end
 end

This requires however every method to return the next state (instead of some useful return value).

So IMHO a better answer to the question would be: "so that the methods can return interesting values instead of the next state". -- MauricioFernandez]

Q: What is the relationship between the State pattern and the Delegator pattern ?

A: Delegation is the most common way of implementing the state pattern. You can implement delegation by hand (like in this example) or use Ruby's Delegator and Forwardable modules. -- SimonVandemoortele

Q: Where should the state transition logic be ? In the state objects (ClientState?) or in the object whose state changes (Context) ?

A: It depends on the situation. I refer you to the Design Patterns book by GHJV (aka the gang of four) for a discussion of the pro and cons of both implementations. -- SimonVandemoortele


Also see: ExampleDesignPatternsInRuby

HomePage | RecentChanges | Preferences | Wikis | RubyGarden
Edit text of this page | View other revisions
Rev 518, Last edited at March 30, 2008 15:26 pm by anonymous / none (diff)
Find: