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 = [
4hardware_controller</span>.green, <span class="no"> <strong>5</strong></span> <span class="iv">hardware_controller.yellow,
6hardware_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">"</span><span class="k">Invalid state!</span><span class="dl">"</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]
24hardware_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):
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