state_pattern

A Ruby state pattern implementation.

This library intentionally follows the classic state pattern implementation (no mixins, classical delegation to simple state classes, etc.) believing that it increases flexibility (internal DSL constraints vs plain object oriented Ruby power), simplicity and clarity.

The gem is ready for Rails active record integration (see below and the examples folder).

Usage and functionality summary

  • Define the set of states you want your stateful object to have by creating a class for each state and inheriting from StatePattern:State.

  • All public methods defined in this state classes, except enter and exit (see below), are then available to the stateful object and their behaviour will depend on the current state .

  • If this automatic delegation to the current state public methods is not enough for your stateful object then you can just reopen the method and use super whenever you want to call the state implementation.

  • Inside each state instance you can access the stateable object through the stateful method.

  • Inside each state instance you can access the previous state through the previous_state method.

  • Define enter or exit methods to hook any behaviour you want to execute whenever the stateful object enters or exits the state.

  • An event is just a method that calls transition_to at some point.

  • If you want guards for some event just use plain old ifs before your transition_to.

  • In the stateful object you must set_initial_state.

Examples

So here’s a simple example that mimics a traffic semaphore

require 'rubygems'
require 'state_pattern'

class Stop < StatePattern::State
  def next
    sleep 3
    transition_to(Go)
  end

  def color
    "Red"
  end
end

class Go < StatePattern::State
  def next
    sleep 2
    transition_to(Caution)
  end

  def color
    "Green"
  end
end

class Caution < StatePattern::State
  def next
    sleep 1
    transition_to(Stop)
  end

  def color
    "Amber"
  end
end

class TrafficSemaphore
  include StatePattern
  set_initial_state Stop
end

semaphore = TrafficSemaphore.new

loop do
  puts semaphore.color
  semaphore.next
end

Let’s now use one nice example from the AASM documentation and translate it to state_pattern.

require 'rubygems'
require 'state_pattern'

class Dating < StatePattern::State
  def get_intimate
    transition_to(Intimate) if stateable.drunk?
  end

  def get_married
    transition_to(Married) if stateable.willing_to_give_up_manhood?
  end

  def enter
    stateable.make_happy
  end

  def exit
    stateable.make_depressed
  end
end

class Intimate < StatePattern::State
  def get_married
    transition_to(Married) if stateable.willing_to_give_up_manhood?
  end

  def enter
    stateable.make_very_happy
  end

  def exit
    stateable.never_speak_again
  end
end

class Married < StatePattern::State
  def enter
    stateable.give_up_intimacy
  end

  def exit
    stateable.buy_exotic_car_and_wear_a_combover
  end
end

class Relationship
  include StatePattern
  set_initial_state Dating

  def drunk?; @drunk; end
  def willing_to_give_up_manhood?; @give_up_manhood; end
  def make_happy; end
  def make_depressed; end
  def make_very_happy; end
  def never_speak_again; end
  def give_up_intimacy; end
  def buy_exotic_car_and_wear_a_combover; end
end

Enter and exit hooks

Inside your state classes, any code that you put inside the enter method will be executed when the state is instantiated. You can also use the exit hook which is triggered when a successful transition to another state takes place.

Overriding automatic delegation

If the automatic delegation to the current state public methods is not enough for your stateful object then you can just reopen the method and use super whenever you want to call the state implementation.

class TrafficSemaphore
  include StatePattern
  set_initial_state Stop

  def color
    # some great code here
    #this calls the current state implementation
    super 
    # more cool hacking here
  end
end

Rails

To use the state pattern in your Rails models you need to:

  • Add a state column for your model table of type string

  • Include StatePattern::ActiveRecord in your model file

  • Use the state pattern as you would do in a plain Ruby class as shown above

Please see the examples folder for a Rails 3 example.

Example

Note this is not the best example to show as ideally this plugin should be used with lot of state dependent behaviour and this is not the case. Remember to put each class in its correct file following Rails naming conventions.

module BlogStates
  class StateBase < StatePattern::State
    def submit!
    end

    def publish!
    end

    def reject!
      transition_to(Rejected)
      stateable.save!
    end

    def verify!
    end
  end

  class Published < StateBase
  end

  class Pending < StateBase
    def publish!
      transition_to(Published) if stateable.valid?
      stateable.save!
    end
  end

  class Unverified < StateBase
    def submit!
      if stateable.submitter.manager?
        if stateable.profile_complete?
          transition_to(Published)
        else
          transition_to(Pending)
        end

        stateable.save!
      end
    end

    def verify!
      transition_to(Pending)
      stateable.save!
    end
  end

  class Rejected < StateBase
    def publish!
      transition_to(Published) if stateable.valid?
      stateable.save!
    end

    def enter
      Notifier.notify_blog_owner(stateable)
    end
  end
end

class Blog < ActiveRecord::Base
  include StatePattern::ActiveRecord
  set_initial_state Unverified

   .
   .
   .

end

The state attribute

By default StatePattern::ActiveRecord expects a column named ‘state’ in the model. If you prefer to use another attribute do:

set_state_attribute :state_column

How do I decide? state_pattern or AASM?

  • Lot of state dependent behavior? Lot of conditional logic depending on the state? => state_pattern

  • Not much state dependent behavior? => AASM

Thanks

Installation

sudo gem install state_pattern

Collaborate

github.com/dcadenas/state_pattern

Copyright © 2009 Daniel Cadenas. See LICENSE for details.