golem_statemachine 0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +472 -0
- data/Rakefile +23 -0
- data/examples/document.rb +44 -0
- data/examples/monster.rb +100 -0
- data/examples/seminar.rb +138 -0
- data/examples/seminar_enrollment.rb +141 -0
- data/init.rb +5 -0
- data/install.rb +1 -0
- data/lib/golem.rb +207 -0
- data/lib/golem/dsl/decision_def.rb +38 -0
- data/lib/golem/dsl/event_def.rb +89 -0
- data/lib/golem/dsl/state_def.rb +34 -0
- data/lib/golem/dsl/state_machine_def.rb +73 -0
- data/lib/golem/dsl/transition_def.rb +51 -0
- data/lib/golem/model/callback.rb +32 -0
- data/lib/golem/model/condition.rb +12 -0
- data/lib/golem/model/event.rb +12 -0
- data/lib/golem/model/state.rb +26 -0
- data/lib/golem/model/state_machine.rb +224 -0
- data/lib/golem/model/transition.rb +37 -0
- data/lib/golem/util/element_collection.rb +33 -0
- data/tasks/golem_statemachine_tasks.rake +4 -0
- data/test/active_record_test.rb +189 -0
- data/test/dsl_test.rb +429 -0
- data/test/monster_test.rb +110 -0
- data/test/problematic_test.rb +95 -0
- data/test/seminar_test.rb +106 -0
- data/test/statemachine_assertions.rb +79 -0
- data/test/test_helper.rb +5 -0
- data/uninstall.rb +1 -0
- metadata +119 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 [name of plugin creator]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,472 @@
|
|
1
|
+
= Golem Statemachine
|
2
|
+
|
3
|
+
Golem adds {Finite State Machine (FSM)}[http://en.wikipedia.org/wiki/Finite_state_machine] behaviour to Ruby classes.
|
4
|
+
Basically, you get a nice DSL (domain-specific language) for defining the FSM rules, and some functionality to enforce
|
5
|
+
those rules in your objects. Although Golem was designed specifically with ActiveRecord in mind, it should work with
|
6
|
+
any Ruby object.
|
7
|
+
|
8
|
+
The Finite State Machine pattern has many potential uses, but in practice you'll probably find it most useful in
|
9
|
+
implementing complex business logic -- the kind that requires multi-page UML diagrams describing an entity's behavior
|
10
|
+
over a series of events. Golem makes it much easier to implement and keep track of complicated, stateful behaviour,
|
11
|
+
and the DSL you use to define your state machine in Ruby is specifically designed to make translation to and from UML
|
12
|
+
easy.
|
13
|
+
|
14
|
+
|
15
|
+
==== Contents
|
16
|
+
|
17
|
+
1. <b>Installation</b>
|
18
|
+
2. <b>A Trivial Example: The ON/OFF Switch</b>
|
19
|
+
3. <b>The DSL Syntax: A Tutorial</b>
|
20
|
+
4. <b>Using Golem with ActiveRecord</b>
|
21
|
+
5. <b>A Real-World Example: Seminar Registration</b>
|
22
|
+
6. <b>Multiple Statemachines in the Same Class/Model</b>
|
23
|
+
7. <b>Gollem vs. AASM</b>
|
24
|
+
|
25
|
+
== 1. Installation
|
26
|
+
|
27
|
+
Install as a Rails plugin:
|
28
|
+
|
29
|
+
script/plugin install git://github.com/zuk/golem_statemachine.git
|
30
|
+
|
31
|
+
If using Golem in an ActiveRecord model:
|
32
|
+
|
33
|
+
class Example < ActiveRecord::Base
|
34
|
+
|
35
|
+
include Golem
|
36
|
+
|
37
|
+
define_statemachine do
|
38
|
+
# ... write your statemachine definition ...
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
Also make sure that the underlying SQL table has a <tt>state</tt> column of type <tt>string</tt> (varchar).
|
44
|
+
If you want to store the state in a different column, use <tt>state_attribute</tt> like this:
|
45
|
+
|
46
|
+
define_statemachine do
|
47
|
+
state_attribute :status
|
48
|
+
|
49
|
+
# ...
|
50
|
+
end
|
51
|
+
|
52
|
+
For plain old Ruby classes, everything works the same way, except the state is not persisted, only stored in the
|
53
|
+
object's instance variable (<tt>@state</tt>, by default).
|
54
|
+
|
55
|
+
|
56
|
+
=== 2. A Trivial Example: The ON/OFF Switch
|
57
|
+
|
58
|
+
A light switch is initially in an "off" state. When you flip the switch, it transitions to an "on" state. A subsequent "flip switch" event returns it back to an off state.
|
59
|
+
|
60
|
+
Here's the UML state machine diagram of an on/off switch:
|
61
|
+
|
62
|
+
http://cloud.github.com/downloads/zuk/golem_statemachine/on_off_switch_UML.png
|
63
|
+
|
64
|
+
And here's what this looks like in Ruby code using Golem:
|
65
|
+
|
66
|
+
require 'golem'
|
67
|
+
|
68
|
+
class LightSwitch
|
69
|
+
include Golem
|
70
|
+
|
71
|
+
define_statemachine do
|
72
|
+
initial_state :OFF
|
73
|
+
|
74
|
+
state :OFF do
|
75
|
+
on :flip_switch, :to => :ON
|
76
|
+
end
|
77
|
+
|
78
|
+
state :ON do
|
79
|
+
on :flip_switch, :to => :OFF
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
switch = LightSwitch.new
|
87
|
+
puts switch.current_state # ==> :OFF
|
88
|
+
switch.flip_switch
|
89
|
+
puts switch.current_state # ==> :ON
|
90
|
+
switch.flip_switch
|
91
|
+
puts switch.current_state # ==> :OFF
|
92
|
+
|
93
|
+
|
94
|
+
=== 3. The DSL Syntax: A Tutorial
|
95
|
+
|
96
|
+
To define a statemachine (inside a Ruby class definition, after including the Golem module), place your definition
|
97
|
+
inside the <tt>define_statemachine</tt> block:
|
98
|
+
|
99
|
+
require 'golem'
|
100
|
+
|
101
|
+
class Monster
|
102
|
+
include Golem
|
103
|
+
define_statemachine do
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
Now to create some states:
|
109
|
+
|
110
|
+
http://cloud.github.com/downloads/zuk/golem_statemachine/monster_1_UML.png
|
111
|
+
|
112
|
+
class Monster
|
113
|
+
include Golem
|
114
|
+
define_statemachine do
|
115
|
+
initial_state :HUNGRY
|
116
|
+
state :HUNGRY
|
117
|
+
state :SATIATED
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
And an event:
|
122
|
+
|
123
|
+
http://cloud.github.com/downloads/zuk/golem_statemachine/monster_2_UML.png
|
124
|
+
|
125
|
+
class Monster
|
126
|
+
include Golem
|
127
|
+
define_statemachine do
|
128
|
+
|
129
|
+
state :HUNGRY do
|
130
|
+
on :eat, :to => :SATIATED
|
131
|
+
end
|
132
|
+
|
133
|
+
state :SATIATED
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
The block for each state describes what will happen when a given event occurs. In this case, if the monster is in the
|
138
|
+
<tt>HUNGRY</tt> state and the <tt>eat</tt> event occurs, the monster becomes <tt>SATIATED</tt>.
|
139
|
+
|
140
|
+
Now to make things a bit more interesting:
|
141
|
+
|
142
|
+
http://cloud.github.com/downloads/zuk/golem_statemachine/monster_3_UML.png
|
143
|
+
|
144
|
+
class Monster
|
145
|
+
include Golem
|
146
|
+
|
147
|
+
attr_accessor :state
|
148
|
+
|
149
|
+
def initialize(name)
|
150
|
+
@name = name
|
151
|
+
end
|
152
|
+
|
153
|
+
def to_s
|
154
|
+
@name
|
155
|
+
end
|
156
|
+
|
157
|
+
def likes?(food)
|
158
|
+
food.kind_of?(String)
|
159
|
+
end
|
160
|
+
|
161
|
+
define_statemachine do
|
162
|
+
initial_state :HUNGRY
|
163
|
+
|
164
|
+
state :HUNGRY do
|
165
|
+
on :eat do
|
166
|
+
transition :to => :SATIATED do
|
167
|
+
guard do |monster, food|
|
168
|
+
monster.likes?(food)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
transition :to => :HUNGRY do
|
172
|
+
action do |monster|
|
173
|
+
puts "#{monster} says BLAH!!"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
state :SATIATED
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
Here the monster becomes <tt>SATIATED</tt> only if it likes the food that it has been given. The <tt>guard</tt>
|
184
|
+
condition takes a block of code that checks whether the monster likes the food. To better illustrate how this works,
|
185
|
+
here's how we would use our Monster statemachine:
|
186
|
+
|
187
|
+
monster = Monster.new("Stringosaurus")
|
188
|
+
|
189
|
+
monster.eat(12345) # ==> "Stringosaurus says BLAH!!"
|
190
|
+
puts monster.state # ==> "HUNGRY"
|
191
|
+
monster.eat("abcde")
|
192
|
+
puts monster.state # ==> "SATIATED"
|
193
|
+
|
194
|
+
Finally, every state can have an <tt>enter</tt> and <tt>exit</tt> action that will be executed whenever that state
|
195
|
+
is entered or exited. This can be a block, a callback method (as a Symbol), or a Proc/lambda. Also, in the interest
|
196
|
+
of leaner code, we rewrite things using more compact syntax:
|
197
|
+
|
198
|
+
http://cloud.github.com/downloads/zuk/golem_statemachine/monster_4_UML.png
|
199
|
+
|
200
|
+
class Monster
|
201
|
+
include Golem
|
202
|
+
|
203
|
+
def initialize(name)
|
204
|
+
@name = name
|
205
|
+
end
|
206
|
+
|
207
|
+
def to_s
|
208
|
+
@name
|
209
|
+
end
|
210
|
+
|
211
|
+
def likes?(food)
|
212
|
+
food.kind_of?(String)
|
213
|
+
end
|
214
|
+
|
215
|
+
define_statemachine do
|
216
|
+
initial_state :HUNGRY
|
217
|
+
|
218
|
+
state :HUNGRY do
|
219
|
+
on :eat do
|
220
|
+
transition :to => :SATIATED, :if => :likes?
|
221
|
+
transition :to => :HUNGRY do
|
222
|
+
action {|monster| puts "#{monster} says BLAH!!"}
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
state :SATIATED do
|
228
|
+
enter {|monster| puts "#{monster} says BURP!!"}
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
For a full list of commands available inside the <tt>define_statemachine</tt> block, have a look at the code in
|
234
|
+
<tt>golem/dsl</tt> (starting with <tt>golem/dsl/state_machine_def.rb</tt>).
|
235
|
+
|
236
|
+
|
237
|
+
=== 4. Using Golem with ActiveRecord
|
238
|
+
|
239
|
+
When you include Golem in an ActiveRecord class, several AR-specific functions are automatically enabled:
|
240
|
+
|
241
|
+
1. State changes are automatically saved to the database. By default it is expected that your ActiveRecord model has a
|
242
|
+
<tt>state</tt> column, although you can change the column where the state is stored using the <tt>state_attribute</tt>
|
243
|
+
declaration.
|
244
|
+
2. When an event is fired, upon completion the <tt>save</tt> or <tt>save!</tt> method is automatically called
|
245
|
+
(<tt>save</tt> if you call the regular event trigger, and <tt>save!</tt> if you use the exclamation trigger: e.g.
|
246
|
+
<tt>open</tt> and <tt>open!</tt> respectively).
|
247
|
+
3. When using the regular event trigger, any transition errors are recorded and checked during record validation, so
|
248
|
+
that calling <tt>valid?</tt> will add to the record's <tt>errors</tt> collection if transition errors occured during
|
249
|
+
event calls.
|
250
|
+
4. Event triggers that result in successful transitions return true; unsuccessful triggers return false (similar to the
|
251
|
+
behaviour of ActiveRecord's <tt>save</tt> method. If using the exclamation triggers (e.g. <tt>open!</tt> rather than
|
252
|
+
just <tt>open</tt>), a Golem::ImpossibleEvent exception is raised on transition failure. (This last functionality
|
253
|
+
is true whether you're using ActiveRecord or not, but it is meant to be useful in the context of standard ActiveRecord
|
254
|
+
usage.)
|
255
|
+
|
256
|
+
=== 5. A Real-World Example: Seminar Registration
|
257
|
+
|
258
|
+
Monsters and On/Off switches are all well end good, but once you get your head around how a finite state machine works,
|
259
|
+
you'll probably want to do something a little more useful. Here's an example of a course registration system, adapted
|
260
|
+
from {Scott W. Ambler's primer on UML2 State Machine Diagrams}[http://www.agilemodeling.com/artifacts/stateMachineDiagram.htm]:
|
261
|
+
|
262
|
+
The UML state machine diagram:
|
263
|
+
|
264
|
+
http://cloud.github.com/downloads/zuk/golem_statemachine/seminar_enrollment_UML.png
|
265
|
+
|
266
|
+
The Ruby implementation (see blow for discussion):
|
267
|
+
|
268
|
+
require 'golem'
|
269
|
+
|
270
|
+
class Seminar
|
271
|
+
attr_accessor :status
|
272
|
+
attr_accessor :students
|
273
|
+
attr_accessor :waiting_list
|
274
|
+
attr_accessor :max_class_size
|
275
|
+
attr_accessor :notifications_sent
|
276
|
+
|
277
|
+
@@out = STDOUT
|
278
|
+
|
279
|
+
def self.output=(output)
|
280
|
+
@@out = output
|
281
|
+
end
|
282
|
+
|
283
|
+
def initialize
|
284
|
+
@students = [] # list of students enrolled in the course
|
285
|
+
@max_class_size = 5
|
286
|
+
@notifications_sent = []
|
287
|
+
end
|
288
|
+
|
289
|
+
def seats_available
|
290
|
+
@max_class_size - @students.size
|
291
|
+
end
|
292
|
+
|
293
|
+
def waiting_list_is_empty?
|
294
|
+
@waiting_list.empty?
|
295
|
+
end
|
296
|
+
|
297
|
+
def student_is_enrolled?(student)
|
298
|
+
@students.include? student
|
299
|
+
end
|
300
|
+
|
301
|
+
def add_student_to_waiting_list(student)
|
302
|
+
@waiting_list << student
|
303
|
+
end
|
304
|
+
|
305
|
+
def create_waiting_list
|
306
|
+
@waiting_list = []
|
307
|
+
end
|
308
|
+
|
309
|
+
def notify_waiting_list_that_enrollment_is_closed
|
310
|
+
@waiting_list.each{|student| self.notifications_sent << "#{student}: waiting list is closed"}
|
311
|
+
end
|
312
|
+
|
313
|
+
def notify_students_that_the_seminar_is_cancelled
|
314
|
+
(@students + @waiting_list).each{|student| self.notifications_sent << "#{student}: the seminar has been cancelled"}
|
315
|
+
end
|
316
|
+
|
317
|
+
|
318
|
+
include Golem
|
319
|
+
|
320
|
+
define_statemachine do
|
321
|
+
initial_state :proposed
|
322
|
+
state_attribute :status
|
323
|
+
|
324
|
+
state :proposed do
|
325
|
+
on :schedule, :to => :scheduled
|
326
|
+
end
|
327
|
+
|
328
|
+
state :scheduled do
|
329
|
+
on :open, :to => :open_for_enrollment
|
330
|
+
end
|
331
|
+
|
332
|
+
state :open_for_enrollment do
|
333
|
+
on :close, :to => :closed_to_enrollment
|
334
|
+
on :enroll_student do
|
335
|
+
transition do
|
336
|
+
guard {|seminar, student| !seminar.student_is_enrolled?(student) && seminar.seats_available > 1 }
|
337
|
+
action {|seminar, student| seminar.students << student}
|
338
|
+
end
|
339
|
+
transition :to => :full do
|
340
|
+
guard {|seminar, student| !seminar.student_is_enrolled?(student) }
|
341
|
+
action do |seminar, student|
|
342
|
+
seminar.create_waiting_list
|
343
|
+
if seminar.seats_available == 1
|
344
|
+
seminar.students << student
|
345
|
+
else
|
346
|
+
seminar.add_student_to_waiting_list(student)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
on :drop_student do
|
352
|
+
transition :if => :student_is_enrolled? do
|
353
|
+
action {|seminar, student| seminar.students.delete student}
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
state :full do
|
359
|
+
on :move_to_bigger_classroom, :to => :open_for_enrollment,
|
360
|
+
:action => Proc.new{|seminar, additional_seats| seminar.max_class_size += additional_seats}
|
361
|
+
# Note that this :if condition applies to all transitions inside the event, in addition to each
|
362
|
+
# transaction's own :if/guard statement.
|
363
|
+
on :drop_student, :if => :student_is_enrolled? do
|
364
|
+
transition :to => :open_for_enrollment, :if => :waiting_list_is_empty? do
|
365
|
+
action {|seminar, student| seminar.students.delete student}
|
366
|
+
end
|
367
|
+
transition do
|
368
|
+
action do |seminar, student|
|
369
|
+
seminar.students.delete student
|
370
|
+
seminar.enroll_student seminar.waiting_list.shift
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
on :enroll_student, :if => Proc.new{|seminar, student| !seminar.student_is_enrolled?(student)} do
|
375
|
+
transition do
|
376
|
+
guard {|seminar, student| seminar.seats_available > 0}
|
377
|
+
action {|seminar, student| seminar.students << student}
|
378
|
+
end
|
379
|
+
transition :action => :add_student_to_waiting_list
|
380
|
+
end
|
381
|
+
on :close, :to => :closed_to_enrollment
|
382
|
+
end
|
383
|
+
|
384
|
+
state :closed_to_enrollment do
|
385
|
+
enter :notify_waiting_list_that_enrollment_is_closed
|
386
|
+
end
|
387
|
+
|
388
|
+
state :cancelled do
|
389
|
+
enter :notify_students_that_the_seminar_is_cancelled
|
390
|
+
end
|
391
|
+
|
392
|
+
# The 'cancel' event can occur in all states.
|
393
|
+
all_states.each do |state|
|
394
|
+
state.on :cancel, :to => :cancelled
|
395
|
+
end
|
396
|
+
|
397
|
+
on_all_transitions do |seminar, event, transition, *event_args|
|
398
|
+
@@out.puts "==[#{event.name}(#{event_args.collect{|arg| arg.inspect}.join(",")})]==> #{transition.from.name} --> #{transition.to.name}"
|
399
|
+
@@out.puts " ENROLLED: #{seminar.students.inspect}"
|
400
|
+
@@out.puts " WAITING: #{seminar.waiting_list.inspect}"
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
|
406
|
+
s = Seminar.new
|
407
|
+
s.schedule!
|
408
|
+
s.open!
|
409
|
+
puts s.status # ====> "open_for_enrollment"
|
410
|
+
s.enroll_student! "bobby"
|
411
|
+
s.enroll_student! "eva"
|
412
|
+
s.enroll_student! "sally"
|
413
|
+
s.enroll_student! "matt"
|
414
|
+
s.enroll_student! "karina"
|
415
|
+
s.enroll_student! "tony"
|
416
|
+
s.enroll_student! "rich"
|
417
|
+
s.enroll_student! "suzie"
|
418
|
+
s.enroll_student! "fred"
|
419
|
+
puts s.status # ====> "full"
|
420
|
+
s.drop_student! "sally"
|
421
|
+
s.drop_student! "bobby"
|
422
|
+
s.drop_student! "tony"
|
423
|
+
s.drop_student! "rich"
|
424
|
+
s.drop_student! "eva"
|
425
|
+
puts s.status # ====> "open_for_enrollment"
|
426
|
+
|
427
|
+
There are a few things to note in the above code:
|
428
|
+
|
429
|
+
1. We use <tt>state_attribute</tt> to tell Golem that the current state will be stored in the <tt>@status</tt> instance
|
430
|
+
variable (by default the state is stored in the <tt>@state</tt> variable).
|
431
|
+
2. We log each transition by specifying a callback function for <tt>on_all_transitions</tt>. The Seminar object's
|
432
|
+
<tt>log_transition</tt> method will be called on each successful transition. The Event that caused the transition,
|
433
|
+
and the Transition itself are automatically passed as the first two arguments to the callback, along with any
|
434
|
+
other arguments that may have been passed in the event trigger.
|
435
|
+
|
436
|
+
|
437
|
+
== 6. Multiple Statemachines in the Same Class/Model
|
438
|
+
|
439
|
+
It's possible to define multiple statemachines in the same class:
|
440
|
+
|
441
|
+
class Foo
|
442
|
+
include Golem
|
443
|
+
|
444
|
+
define_statemachine(:mouth) do
|
445
|
+
# ...
|
446
|
+
end
|
447
|
+
|
448
|
+
define_statemachine(:eye) do
|
449
|
+
# ...
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
In this case the state of the "mouth" statemachine can be retrieved using <tt>mouth_state</tt> and of the "eye" using
|
454
|
+
<tt>nose_state</tt>. You can override the names of these state attributes as usual using <tt>state_attribute</tt>
|
455
|
+
declarations under each statemachine.
|
456
|
+
|
457
|
+
Event triggers are shared across statemachines, so if both of your statemachines define an event called "open",
|
458
|
+
triggering an "open" event on an instance of the class will trigger the event for both statemachines.
|
459
|
+
|
460
|
+
For an example of a class with two statemachines see <tt>examples/monster.rb</tt>.
|
461
|
+
|
462
|
+
== 7. Golem vs. AASM
|
463
|
+
|
464
|
+
There is already another popular FSM implementation for Ruby -- {rubyist's AASM}[http://github.com/rubyist/aasm]
|
465
|
+
(also known as acts_as_state_machine). Golem was developed from scratch as an alternative to AASM, with the intention
|
466
|
+
of a better DSL and cleaner, easier to read code.
|
467
|
+
|
468
|
+
Golem's DSL is centered around States rather than Events; this makes Golem statemachines easier to visualize in UML
|
469
|
+
(and vice-versa). Golem's DSL also implements the decision pseudostate (a concept taken from UML), making complicated
|
470
|
+
business logic easier to implement.
|
471
|
+
|
472
|
+
Golem's code is also more modular and more consistent, which will hopefully make extending the DSL easier.
|