golem_statemachine 0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.