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