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</span>.green, <span class="no"> <strong>5</strong></span> <span class="iv">hardware_controller.yellow,
6 hardware_controller</span>.red <span class="no"> 7</span> ] <span class="no"> 8</span> <span class="no"> 9</span> <span class="r">case</span> gyr <span class="no"><strong>10</strong></span> <span class="r">when</span> [<span class="pc">true</span>, <span class="pc">false</span>, <span class="pc">false</span>] <span class="r">then</span> <span class="sy">:proceed</span> <span class="no">11</span> <span class="r">when</span> [<span class="pc">false</span>, <span class="pc">true</span>, <span class="pc">false</span>] <span class="r">then</span> <span class="sy">:caution</span> <span class="no">12</span> <span class="r">when</span> [<span class="pc">false</span>, <span class="pc">false</span>, <span class="pc">true</span>] <span class="r">then</span> <span class="sy">:stop</span> <span class="no">13</span> <span class="r">else</span> raise <span class="s"><span class="dl">&quot;</span><span class="k">Invalid state!</span><span class="dl">&quot;</span></span> <span class="no">14</span> <span class="r">end</span> <span class="no"><strong>15</strong></span> <span class="r">end</span> <span class="no">16</span> <span class="no">17</span> <span class="r">def</span> <span class="fu">state=</span>(value) <span class="no">18</span> gyr = <span class="r">case</span> value <span class="no">19</span> <span class="r">when</span> <span class="sy">:proceed</span> <span class="r">then</span> [<span class="pc">true</span>, <span class="pc">false</span>, <span class="pc">false</span>] <span class="no"><strong>20</strong></span> <span class="r">when</span> <span class="sy">:caution</span> <span class="r">then</span> [<span class="pc">false</span>, <span class="pc">true</span>, <span class="pc">false</span>] <span class="no">21</span> <span class="r">when</span> <span class="sy">:stop</span> <span class="r">then</span> [<span class="pc">false</span>, <span class="pc">false</span>, <span class="pc">true</span>] <span class="no">22</span> <span class="r">end</span> <span class="no">23</span> <span class="iv">hardware_controller.green = gyr[0]
24 hardware_controller</span>.yellow = gyr[<span class="i">1</span>] <span class="no"><strong>25</strong></span> <span class="iv">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):


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
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:


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
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


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


Projects I work on

Projects I worked on