111 lines
2.8 KiB
Ruby
111 lines
2.8 KiB
Ruby
# Allow an object to alter its behavior when its internal state changes.
|
|
# The object will appear to change its class.
|
|
|
|
# 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
|
|
|
|
|
|
module StatePattern
|
|
class UnknownStateException < Exception
|
|
end
|
|
|
|
def StatePattern.included(base)
|
|
base.extend StatePattern::ClassMethods
|
|
end
|
|
|
|
module ClassMethods
|
|
attr_reader :state_classes
|
|
|
|
def state(state_name, &block)
|
|
@state_classes ||= {}
|
|
|
|
new_klass = Class.new(self, &block)
|
|
new_klass.class_eval do
|
|
alias_method :__old_init, :initialize
|
|
|
|
def initialize(context, *args, &block)
|
|
@context = context
|
|
__old_init(*args, &block)
|
|
end
|
|
end
|
|
|
|
@state_classes[state_name] = new_klass
|
|
end
|
|
end
|
|
|
|
attr_accessor :current_state, :current_state_obj
|
|
|
|
def transition_to(state_name, *args, &block)
|
|
new_context = @context || self
|
|
klass = new_context.class.state_classes[state_name]
|
|
if klass
|
|
new_context.current_state = state_name
|
|
new_context.current_state_obj = klass.new(new_context, *args, &block)
|
|
else
|
|
raise UnknownStateException,"tried to transition to unknown state,#{state_name}"
|
|
end
|
|
end
|
|
|
|
def method_missing(method, *args, &block)
|
|
unless @current_state_obj
|
|
transition_to :initial
|
|
end
|
|
|
|
if @current_state_obj
|
|
@current_state_obj.send(method, *args, &block)
|
|
else
|
|
super
|
|
end
|
|
end
|
|
end
|
|
|
|
class Connection
|
|
include StatePattern
|
|
|
|
# you always need a state named initial
|
|
state :initial do
|
|
|
|
def connect
|
|
# move to state :connected. all other args to transition_to# are passed
|
|
# to the new state's constructor transition_to:connected, "hello from
|
|
# initial state"
|
|
puts "connected"
|
|
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
|
|
# you can also change the state from outside of the state objects
|
|
# transition_to:initial
|
|
puts "resetting outside a state"
|
|
end
|
|
end
|
|
|
|
# Usage
|
|
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 |