How to Use AlterEgo With ActiveRecord
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