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.
@@ -0,0 +1,3 @@
1
+ *.log
2
+ *.db
3
+ nbproject
@@ -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.
@@ -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.