I stumbled on AlterEgo last week, after Avdi announced it. This library implements the state pattern for any object, not just ActiveRecord models, which acts_as_state_machine (and it’s successor, aasm) do.

But, many people, myself included, want to use AlterEgo in the context of ActiveRecord models. Fortunately, AlterEgo provides us with all the necessary plumbing to do that very easily.

I will reuse Avdi’s traffic light example from the specifications. Avdi already points us to 90% of the solution in his example:

 1 class TrafficLightWithCustomStorage
 2   def state
 3     gyr = [
 4       @hardware_controller.green,
 5       @hardware_controller.yellow,
 6       @hardware_controller.red
 7     ]
 8 
 9     case gyr
10     when [true, false, false] then :proceed
11     when [false, true, false] then :caution
12     when [false, false, true] then :stop
13     else raise "Invalid state!"
14     end
15   end
16 
17   def state=(value)
18     gyr = case value
19           when :proceed  then [true, false, false]
20           when :caution  then [false, true, false]
21           when :stop     then [false, false, true]
22           end
23     @hardware_controller.green  = gyr[0]
24     @hardware_controller.yellow = gyr[1]
25     @hardware_controller.red    = gyr[2]
26   end
27 end

When an object implements #state and #state=, AlterEgo will serialize it’s state using these two methods. Let’s hook this to ActiveRecord (note, I changed the code for a quick example, so it’s not exactly the same, but very similar):

app/models/traffic_light.rb
 1 class TrafficLight < ActiveRecord::Base
 2   def state
 3     case read_attribute(:color)
 4     when "green";   :proceed
 5     when "yellow";  :caution
 6     when "red";     :stop
 7     else;           raise "Invalid color: #{read_attribute(:color).inspect}"
 8     end
 9   end
10 
11   def state=(value)
12     color_value = case value
13     when :proceed;  "green"
14     when :caution;  "yellow"
15     when :stop;     "red"
16     else;           raise "Don't know how to convert #{value.inspect} to color value"
17     end
18     write_attribute(:color, color_value)
19   end
20 end

The most important thing you should check is that your database model has a valid state on model instantiation. Whether you do it using the :default key in your migration or some other way is irrelevant, but the value has to be provided, or the #state method will barf (or implement default processing there?) In the example app, I opted to use a default value:

db/migrate/20081209150159_create_traffic_lights.rb
 1 class CreateTrafficLights < ActiveRecord::Migration
 2   def self.up
 3     create_table :traffic_lights do |t|
 4       t.string :color, :default => "red"
 5     end
 6   end
 7 
 8   def self.down
 9     drop_table :traffic_lights
10   end
11 end

You may check the full code in action in the GitHub repository: alter_ego_plus_active_record

Search

Your Host

A picture of me

I am François Beausoleil, a Ruby on Rails and Scala developer. During the day, I work on Seevibes, a platform to measure social interactions related to TV shows. At night, I am interested many things. Read my biography.

Top Tags

Books I read and recommend

Links

Projects I work on

Projects I worked on